微服务商城系统(十四)微信支付


代码链接: https://github.com/betterGa/ChangGou

一、支付微服务

1、微信支付 API

    微信支付提供了 SDK 和 Demo(然而我并没有找到 sdk,可能微信团队已经把它上传到 Maven 中了):
1537902584152
使用微信支付SDK,在 common 工程的 pom.xml 中中引入依赖

<!--微信支付-->
<dependency>
    <groupId>com.github.wxpay</groupId>
    <artifactId>wxpay-sdk</artifactId>
    <version>0.0.3</version>
</dependency>

我们主要会用到微信支付 SDK 的以下功能:

获取随机字符串

WXPayUtil.generateNonceStr()

MAP 转换为 XML 字符串(自动添加签名)

 WXPayUtil.generateSignedXml(param, partnerkey)

XML 字符串转换为 MAP(当然,Map 也可以转化成 XML)

WXPayUtil.xmlToMap(result)

在 changgou-common工程下引入依赖:

<!--微信支付-->
<dependency>
    <groupId>com.github.wxpay</groupId>
    <artifactId>wxpay-sdk</artifactId>
    <version>0.0.3</version>
</dependency>

进行测试:

public class WeixinPayTest {

    @Test
    public void test() throws Exception {

        // 生成随机字符
        System.out.println("随机字符串" + WXPayUtil.generateNonceStr());

        // 将 Map 转化成 XML
        Map<String, String> dataMap = new HashMap<>();
        dataMap.put("id", "No.1");
        dataMap.put("title", "畅购商城");
        dataMap.put("money", "520");
        String xml = WXPayUtil.mapToXml(dataMap);
        System.out.println(xml);

        // (1)生成签名
        System.out.println("带签名的字符串" + WXPayUtil.generateSignedXml(dataMap, "secret"));

        // 将 XML 转化成 Map
       System.out.println(WXPayUtil.xmlToMap(xml));
    }
}

运行结果:
在这里插入图片描述
     注意到,(1)处,WXPayUtil.generateSignedXml 方法,传 Map 和 密钥 作为参数,可以生成带 签名 的字符串,签名是这么生成的。

2、HttpClient 工具类

     HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。HttpClient 已经应用在很多的项目中,比如 Apache Jakarta 上很著名的另外两个开源项目 Cactus 和 HTMLUnit 都使用了 HttpClient。
    HttpClient 通俗讲就是模拟了浏览器的行为,如果我们需要在后端向某一地址提交数据获取结果,就可以使用 HttpClient.

使用 HttpClient 需要先导入依赖:

   <!--httpclient支持-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

     关于HttpClient(原生)具体的使用不属于我们本章的学习内容,为了简化 HttpClient 的使用,提供了工具类 HttpClient(对原生 HttpClient 进行了封装)。

HttpClient工具类代码:

public class HttpClient {
    private String url;
    private Map<String, String> param;
    private int statusCode;
    private String content;
    private String xmlParam;
    private boolean isHttps;

    public boolean isHttps() {
        return isHttps;
    }

    public void setHttps(boolean isHttps) {
        this.isHttps = isHttps;
    }

    public String getXmlParam() {
        return xmlParam;
    }

    public void setXmlParam(String xmlParam) {
        this.xmlParam = xmlParam;
    }

    public HttpClient(String url, Map<String, String> param) {
        this.url = url;
        this.param = param;
    }

    public HttpClient(String url) {
        this.url = url;
    }

    public void setParameter(Map<String, String> map) {
        param = map;
    }

    public void addParameter(String key, String value) {
        if (param == null)
            param = new HashMap<String, String>();
        param.put(key, value);
    }

    public void post() throws ClientProtocolException, IOException {
        HttpPost http = new HttpPost(url);
        setEntity(http);
        execute(http);
    }

    public void put() throws ClientProtocolException, IOException {
        HttpPut http = new HttpPut(url);
        setEntity(http);
        execute(http);
    }

    public void get() throws ClientProtocolException, IOException {
        if (param != null) {
            StringBuilder url = new StringBuilder(this.url);
            boolean isFirst = true;
            for (String key : param.keySet()) {
                if (isFirst) {
                    url.append("?");
                }else {
                    url.append("&");
                }
                url.append(key).append("=").append(param.get(key));
            }
            this.url = url.toString();
        }
        HttpGet http = new HttpGet(url);
        execute(http);
    }

    /**
     * set http post,put param
     */
    private void setEntity(HttpEntityEnclosingRequestBase http) {
        if (param != null) {
            List<NameValuePair> nvps = new LinkedList<NameValuePair>();
            for (String key : param.keySet()) {
                nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
            }
            http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
        }
        if (xmlParam != null) {
            http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
        }
    }

    private void execute(HttpUriRequest http) throws ClientProtocolException,
            IOException {
        CloseableHttpClient httpClient = null;
        try {
            if (isHttps) {
                SSLContext sslContext = new SSLContextBuilder()
                        .loadTrustMaterial(null, new TrustStrategy() {
                            // 信任所有
                            @Override
                            public boolean isTrusted(X509Certificate[] chain,
                                                     String authType)
                                    throws CertificateException {
                                return true;
                            }
                        }).build();
                SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                        sslContext);
                httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
                        .build();
            } else {
                httpClient = HttpClients.createDefault();
            }
            CloseableHttpResponse response = httpClient.execute(http);
            try {
                if (response != null) {
                    if (response.getStatusLine() != null) {
                        statusCode = response.getStatusLine().getStatusCode();
                    }
                    HttpEntity entity = response.getEntity();
                    // 响应内容
                    content = EntityUtils.toString(entity, Consts.UTF_8);
                }
            } finally {
                response.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            httpClient.close();
        }
    }

    public int getStatusCode() {
        return statusCode;
    }

    public String getContent() throws ParseException, IOException {
        return content;
    }
}

进行测试:

public class HttpClientClass {
    /**
     * 测试 HttpClient 工具类的使用
     */
    @Test
    public void test() throws IOException {
        // 发送 Http 请求
        String url = "https://api.mch.weixin.qq.com/pay/orderquery";
        HttpClient httpClient = new HttpClient(url);
        
        // 发送指定参数
        String xml = "<xml><name>用户</name></xml>";
        httpClient.setXmlParam(xml);

        // 使用 Https 协议
        httpClient.setHttps(true);

        // 发送请求
        httpClient.post();

        // 获取响应数据
        String result = httpClient.getContent();
        System.out.println(result);
    }
}

运行结果:
在这里插入图片描述

3、支付微服务搭建

     需要支付微服务与微信支付服务器进行对接。
     创建 changgou-service-pay 工程。

创建application.yml,配置文件如下:

server:
  port: 18092
spring:
  application:
    name: pay
  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

#微信支付信息配置
weixin:
  appid: wx8397f8696b538317
  partner: 1473426802
  partnerkey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
  notifyurl: http://www.itcast.cn

参数说明:

  • appid:微信公众账号或开放平台 APP 的唯一标识
  • partner:财付通平台的商户账号
  • partnerkey:财付通平台的商户密钥
  • notifyurl::回调地址

提供启动类:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class PayApplication {

    public static void main(String[] args) {
        SpringApplication.run(PayApplication.class, args);
    }
}

二、微信支付二维码生成

     在支付页面上生成支付二维码,并显示订单号和金额,用户拿出手机,打开微信扫描页面上的二维码,然后在微信中完成支付。
在这里插入图片描述
实现思路:
     通过 HttpClient 工具类实现对远程支付接口的调用。
接口链接:https://api.mch.weixin.qq.com/pay/unifiedorder

     具体参数参见 “统一下单” API, 构建参数发送给统一下单的 url ,返回的信息中有支付 url,根据 url 生成二维码,显示的订单号和金额也在返回的信息中。
    
代码实现:

  • 业务层
    提供接口:
public interface WeixinPayService {
    Map createNative(Map<String,String> parameterMap) throws Exception;
}

实现:

@Service
public class WeiXinPayServiceImpl implements WeixinPayService {

    // 应用 ID
    @Value("${weixin.appid}")
    private String appid;

    // 商户 ID
    @Value("${weixin.partner}")
    private String partner;

    // 密钥
    @Value("${weixin.partnerkey}")
    private String partnerkey;

    // 支付回调地址
    @Value("${weixin.notifyurl}")
    private String notifyurl;


    /**
     * 创建二维码
     *
     * @param parameterMap
     * @return
     */
    @Override
    public Map createNative(Map<String, String> parameterMap) throws Exception {

        /**
         * 封装参数
         */
        Map<String, String> paramMap = new HashMap<>();

        // 公众账号 ID
        paramMap.put("appid", appid);

        // 商户号
        paramMap.put("mch_id", partner.trim());

        // 随机字符串
        paramMap.put("nonce_str", WXPayUtil.generateNonceStr());

        // 商品描述
        paramMap.put("body", "畅购商城");

        // 商品订单号
        paramMap.put("out_trade_no", parameterMap.get("outtradeno"));

        // 标价金额
        paramMap.put("total_fee", parameterMap.get("totalfee"));

        // 终端 IP
        paramMap.put("spbill_create_ip", "127.0.0.1");

        // 通知地址
        paramMap.put("notify_url", notifyurl);

        // 交易类型
        paramMap.put("trade_type", "NATIVE");

        // 传入密钥,生成的 xml 中就会带有签名 sign 信息
        String xmlParameters = WXPayUtil.generateSignedXml(paramMap, partnerkey);
        System.out.println("xml:"+xmlParameters);

        /**
         * URL
         */
        String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";

        /**
         * 提交方式
          */
        HttpClient httpClient = new HttpClient(url);
        httpClient.setHttps(true);

        /**
         * 提交参数
         */
        httpClient.setXmlParam(xmlParameters);

        /**
         * 执行请求
         */
        httpClient.post();

        // 返回数据
        String content = httpClient.getContent();

        Map<String,String> resultMap=WXPayUtil.xmlToMap(content);
        return resultMap;
    }
}
  • 控制层
@RestController
@RequestMapping(value = "/weixin/pay")
@CrossOrigin
public class WeiXinPayController {

    @Autowired
    private WeixinPayService weixinPayService;

    @GetMapping(value = "/create/native")
    Result createNative(@RequestParam Map<String, String> parameterMap) throws Exception {
        Map resultMap = weixinPayService.createNative(parameterMap);
        return new Result(true, StatusCode.OK,"创建二维码预付订单成功!",resultMap);
    }
}

测试如下:
在这里插入图片描述

打开支付页面 /pay.html,修改 value 路径,改为响应的 code_url 的值:
1558476420961
这时访问 pay.html 页面,会出现付款码,金额为 0.1 元。

三、检测支付状态

     当用户支付成功后跳转到成功页面:

在这里插入图片描述

当返回异常时跳转到错误页面:

在这里插入图片描述

     本身服务器会给商户后台系统发状态信息,但是可能有差错,导致商户后台系统没收到,所以需要主动向服务器查询状态信息。

实现思路:通过 HttpClient 工具类实现对远程接口的调用。
接口链接:https://api.mch.weixin.qq.com/pay/orderquery

具体参数参见 “查询订单” API:
在这里插入图片描述

代码实现:

  • 业务层
         在 com.changgou.service.WeixinPayService 提供方法:
/***
 * 查询订单状态
 * @param out_trade_no : 客户端自定义订单编号
 * @return
 */
public Map queryPayStatus(String out_trade_no);

实现:

/***
 * 查询订单状态
 * @param out_trade_no : 客户端自定义订单编号
 * @return
 */
@Override
public Map queryPayStatus(String out_trade_no) {
    /**
     * 查询订单状态
     * @param out_trade_no
     * @return
     */
    @Override
    public Map queryPayStatus(String out_trade_no) {
        try {
            //1.封装参数
            Map param = new HashMap();
            param.put("appid",appid);                            //应用ID
            param.put("mch_id",partner);                         //商户号
            param.put("out_trade_no",out_trade_no);              //商户订单编号
            param.put("nonce_str",WXPayUtil.generateNonceStr()); //随机字符

            //2、将参数转成 xml 字符,并携带签名
            String paramXml = WXPayUtil.generateSignedXml(param,partnerkey);

            //3、发送请求
            HttpClient httpClient = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
            httpClient.setHttps(true);
            httpClient.setXmlParam(paramXml);
            httpClient.post();

            //4、获取返回值,并将返回值转成 Map
            String content = httpClient.getContent();
            return WXPayUtil.xmlToMap(content);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
  • 控制层
/***
     * 查询支付状态
     * @param outtradeno
     * @return
     */
    @GetMapping(value = "/status/query")
    public Result queryStatus(String outtradeno) {
        Map<String, String> resultMap = weixinPayService.queryPayStatus(outtradeno);
        return new Result(true, StatusCode.OK, "查询状态成功!", resultMap);
    }

运行结果:
在这里插入图片描述

四、内网穿透

     现在系统还有个问题需要解决:微信支付服务器需要访问本地服务器的,让外网访问到本地服务器,就需要用到内网穿透技术 NAT 。使用动态域名解析工具 花生壳 。
新建映射:
在这里插入图片描述

    其中,内网主机即本地 IP,外网域名用的是花生壳赠送的,在 “域名列表” 中可以看到。
在这里插入图片描述
    可以看到,提供了一个访问地址。

要在诊断无误的情况下使用:
在这里插入图片描述

测试访问地址:
在这里插入图片描述
    

五、支付结果通知

1、支付结果回调通知

提供控制层:

  @RequestMapping(value = "/notify/url")
    public String notifyUrl(HttpServletRequest request) throws Exception {

        // 获取网络输入流
        ServletInputStream inputStream = request.getInputStream();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = inputStream.read(buffer)) != -1) {
            baos.write(buffer, 0, len);
        }

        // 微信支付结果的字节数据
      byte[] bytes = baos.toByteArray();

        String xmlResult = new String(bytes, "utf-8");
        System.out.println("微信支付结果的 xml" + xmlResult);

        Map<String, String> resultMap = WXPayUtil.xmlToMap(xmlResult);
        System.out.println("微信支付结果的 Map" + resultMap);

        String result = "<xml>\n" +
                "  <return_code><![CDATA[SUCCESS]]></return_code>\n" +
                "  <return_msg><![CDATA[OK]]></return_msg>\n" +
                "</xml>";

        return result;
    }

需要在 application.yml 中配置访问地址,将该方法对应的路径作为配置文件中 weixin:notifyurl 的属性值。:
在这里插入图片描述
进行测试:
首先需要创建订单:
在这里插入图片描述
     把 “code_url” 放到 pay.html 中,生成付款码,用微信扫描后付款,这时,会在控制台看到输出:
在这里插入图片描述
    注意:方法是 @RequestMapping ,如果误写成 @GetMapping ,控制台输出:
在这里插入图片描述
它说不支持 @PostMapping,那我就改成 @PostMapping:
在这里插入图片描述
     可以看到,和使用 @RequestMapping 的效果是一样的,说明调用这个 notifyurl 的地方,是使用 Post 方法。

     至此,在 pay 项目中就可以获取到支付结果,但是 order 项目并没有获取到结果。需要让 order 项目监听 MQ,当监听到支付成功后,需要把订单状态改为 支付成功。

2、Rabbit MQ 配置

需要先在 pay 工程中导入依赖:

	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
     </dependency>
(1)发送支付状态

    
在 application.yml 中进行配置:

mq:
  pay:
    exchange:
      order: exchange.order
    queue:
      order: queue.order
    routing:
      key: queue.order

在这里插入图片描述

    
在这里插入图片描述
     需要清空 Exchanges 和 Queues。
     本项目中使用代码生成交换机和队列(实际企业开发应该是去 MQ 中生成的),在 pay 工程中 提供一个配置类:

@Configuration
public class MQConfig {


    // 读取配置文件中的内容对象
    @Autowired
    private Environment environment;


    // 创建队列
    @Bean
    public Queue orderQueue() {
       return new Queue(environment.getProperty("mq.pay.queue.order"));   
      }

    // 创建交换机
    @Bean
    public Exchange orderExchange() {

        // 持久化,不自动删除
        return new DirectExchange(environment.getProperty("mq.pay.exchange.order"), true, false);
    }

    // 绑定
    public Binding binding(Queue queue, Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange)
                .with(environment.getProperty("mq.pay.routing.key"))
                .noargs();
    }
}

在 WeiXinPayController 的 notifyUrl 方法中,加入 把支付结果发送给 MQ 的逻辑:
在这里插入图片描述
还需要在 order 工程中,同样需要配置 RabbitMQ 参数,导入 spring-boot-starter-amqp 依赖。

(2)监听

提供监听类:

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

    /**
     * 支付结果监听
     */
    @RabbitHandler
    public void getMessage(String message){

        // 支付结果
        Map<String,String> resultMap = JSON.parseObject(message, Map.class);

        // 输出监听到的消息
        System.out.println(resultMap);

        // 通信标识
        String returnCode=resultMap.get("return_code");

        if(returnCode.equals("SUCCESS")){
            // 业务结果
            String resultCode = resultMap.get("result_code");

            // 订单号
            String outTradeNo = resultMap.get("out_trade_no");

            // 支付成功
            if(resultCode.equals("SUCCESS")){

            }else {
                // 如果支付失败,需要关闭订单,回滚库存
            }
        }
    }
}

     注意:需要启动 pay 工程,生成付款码,然后用户进行支付,创建队列 queue.order,启动 order 工程,监听队列。不可以同时启动两个工程,否则 生产者模块所在的 pay 工程可以正常运行,但是消费者模块所在的 order 会报错误:

org.springframework.amqp.rabbit.listener.BlockingQueueConsumer$DeclarationException: Failed to declare queue(s):[topic.man] at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.attemptPassiveDeclarations(BlockingQueueConsumer.java:700) [spring-rabbit-2.2.7.RELEASE.jar:2.2.7.RELEASE]
    at 			   	org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.passiveDeclarations(Blocki	ngQueueConsumer.java:584) [spring-rabbit-2.2.7.RELEASE.jar:2.2.7.RELEASE] 

     因为此时还未付款,就不会在 rabbitmq 服务器里面创建还不存在的交换机和队列。仅限于第一次启动的时候,以后 rabbitmq 里面以及有对应的交换机和队列存在了,就不用这样做了。
    
付款后,可以看到 RabbitMQ 中新建队列和交换机成功了:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、修改订单状态

     接下来,需要根据支付的结果修改订单信息:
在这里插入图片描述
    
在 OrderService 中提供方法:

    /**
     * 修改订单状态
     *
     * @param outradeno
     * @param paytime
     * @param transcationid
     */
    void updateStatus(String outradeno, String paytime, String transcationid) throws ParseException;


    /**
     * 删除【逻辑删除,其实是修改订单状态】订单信息,回滚库存
     *
     * @param outradeno
     */
    void deleteOrder(String outradeno);

实现:

    /**
     * 修改订单状态
     *
     * @param outradeno
     * @param paytime
     * @param transcationid
     */
    @Override
    public void updateStatus(String outradeno, String paytime, String transcationid) throws ParseException {

        // 查询订单
        Order order = orderMapper.selectByPrimaryKey(outradeno);

        // 时间转换
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss");

        // 获取支付时间
        Date payTime = simpleDateFormat.parse(paytime);

        // 修改订单信息
        order.setPayTime(payTime);
        order.setPayStatus("1");
        order.setTransactionId(transcationid);

        orderMapper.updateByPrimaryKey(order);
    }

    /**
     * 删除订单
     * @param outradeno
     */
    @Override
    public void deleteOrder(String outradeno) {
        // 查询订单
        Order order = orderMapper.selectByPrimaryKey(outradeno);

        // 支付失败
        order.setOrderStatus("2");

        orderMapper.updateByPrimaryKey(order);

        // 回滚库存,需要调用 goods 微服务,未实现

        // 微信支付服务器关闭订单,未实现
    }

如果用户订单支付失败了,或者支付超时了,我们需要删除用户订单,删除订单的同时需要取消订单、回滚库存:
在这里插入图片描述

七、超时订单处理

    现在还有个问题,如果用户下单 30 分钟后还没有支付,需要把订单取消。(超过 5 分钟取消订单,是可以实现的)
在这里插入图片描述
     那么怎么让系统知道 30 分钟后用户是否支付成功了呢?可以轮询,但是轮询非常浪费资源,而且轮询的间隔不好控制;可以用延时队列。
     Rabbit MQ 本身不支持延时队列,不过可以自己实现,有两种方式:
(1)利用 2 个特性:Time To Live( TTL)、Dead Letter Exchange(DLX)、 [A 队列过期-> 转发给 B 队列]
(2)利用 RabbitMQ 中的插件 x-delay-message
     采用第一种方式来实现延时队列,实际上是用队列的超时特性:
在这里插入图片描述
     监听 Queue2 即可。
代码实现:

@Configuration
public class QueueConfig {

    // 创建 queue1
    @Bean
    public Queue orderDelayQueue(){


        return QueueBuilder.durable("orderDelayQueue")

                // queue1 消息过期,进入到死信【没被读取的消息】队列,需要绑定交换机
                .withArgument("x-dead-letter-exchange","orderListenerExchange")

                // queue1 消息过期,会路由到 queue2
                .withArgument("x-dead-letter-routing-key","orderListenerQueue")
                .build();

    }

    // 创建 queue2
    @Bean
    public Queue orderListenerQueue(){
        // 持久化
        return new Queue("orderListenerQueue",true);

    }

    @Bean
    public Exchange orderListenerExchange(){
        return new DirectExchange("orderListenerExchange");
    }


    // 绑定
    @Bean
    public Binding binding(Queue orderListenerQueue,Exchange orderListenerExchange){
        return BindingBuilder.bind(orderListenerQueue)
                .to(orderListenerExchange)
                .with("orderListenerQueue")
                .noargs();
    }
}

下单时发送消息:
在这里插入图片描述
提供监听:

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

    @RabbitHandler
    public void delayMessage(String message) {

        System.out.println("监听到的消息" + message);

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("监听到消息时间" + simpleDateFormat.format(new Date()));
    }
}

这时进行测试,启动 order 工程,可以看到 RabbitMQ 中生成了队列和交换机:
在这里插入图片描述
在这里插入图片描述
接下来进行测试。
先下单:
在这里插入图片描述
可以看到,在控制台输出:
在这里插入图片描述

八、关闭订单与回滚库存

    在 (五)支付结果通知,队列监听方法中,当监听到消息的 result_code 不为 SUCCESS 时,说明支付失败,需要关闭订单、回滚库存。回滚库存的逻辑可以参考上一篇文章中,下单-库存变更 的逻辑,在 Redis 中,查询用户对应的订单信息,然后根据 订单 order 查询出订单明细 orderItem,就能得到订单中商品的 num 数量,最后在 sku 商品表中需要把数量加回来:
在这里插入图片描述
需要在 skuController 中提供库存递增的方法:

 @GetMapping(value = "/asc")
    public Result ascCount(@RequestParam Map<String,String> ascMap){
        skuService.ascCount(ascMap);
        return new Result(true,StatusCode.OK,"库存递增成功");
    }

在 SkuService 接口中提供方法:

   void ascCount(Map<String,String> ascMap);

实现:

 @Override
    public void ascCount(Map<String, String> ascMap) {
        for (Map.Entry<String, String> entry : ascMap.entrySet()) {
            // 商品 ID
            Long id = Long.parseLong(entry.getKey());

            // 数量
            Integer num = Integer.parseInt(entry.getValue());

            /**
             * 使用行级锁防止超卖,通过数据库的事务特性保证数据原子性
             */
            int row = skuMapper.ascCount(id, num);
        }
    }

在 SkuMapper 中使用 SQL 语句行级锁:

@Update("update tb_sku set num=num+#{num} where id=#{id}")
    int ascCount(@Param(value = "id") Long id,  @Param(value = "num")Integer num);

提供 feign 调用:
在这里插入图片描述
    关闭订单用微信支付开发文档中提供的 ”关闭订单“ API。
    之前都是使用 HttpClient 工具,这次试着直接用 sdk,可以看到,在 WxPay 中,有相应方法:
在这里插入图片描述
    可以看到,需要传 reqData ,即 向 wxpay post 的数据(和接口文档中是对应的:
     必须有 appid、mch_id、out_trade_no、nonce_str、sign 参数,WeiXinPayServiceImpl 类有从配置文件中读取参数作为属性,所以可以在 WeiXinPayServiceImpl 类中提供方法,把这些参数传进去)。

/**
     * 构建 WXPay 对象
     */
    public WXPay wxPay(){
        return new WXPay(new WXPayConfig() {
            @Override
            public String getAppID() {
                return getAppid();
            }

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

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

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

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

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

    @Override
    public Map cancelOrder(String outTradeNo) throws Exception {
        // 封装参数
        Map<String,String> reqDate=new HashMap<>();
        reqDate.put("out_trade_no", outTradeNo);
        return wxPay().closeOrder(reqDate);
    }

     可以看到,参数齐活了(不需要手动传 nonce_str、sign 是因为在 closeOrder 中,会调用 fillRequestData 方法,使用了 WXPayUtil.generateNonceStr() 作为 “nonce_str” 的值、 使用了 WXPayUtil.generateSignature(…)作为 “sign” 的值)。

需要在 WeiXinPayController 中再提供一个 取消订单的方法以供调用:

@RequestMapping(value = "/cancel/order")
    public Result cancelOrder(@RequestParam String outTradeNo) throws Exception {
        Map resultMap = weixinPayService.cancelOrder(outTradeNo);
        return new Result(true, StatusCode.OK, "取消订单成功", resultMap);
    }

先测试取消订单的方法:
在这里插入图片描述
返回这样的结果是因为订单已支付:
在这里插入图片描述
根据这个错误码,我们知道,关闭订单也是有可能出现错误信息的,不是全部的订单都可以成关闭,所以修改 WeiXinPayController 层的方法:
在这里插入图片描述
想让监听支付结果的 getMessage 方法里调用取消订单的方法,可以使用 Feign 调用:

@FeignClient("weixinpay")
@RequestMapping(value = "/weixin/pay")
public interface WeiXinPayFeign {

    /**
     * 取消订单
     * @param outTradeNo
     * @return
     */

    @RequestMapping(value = "/cancel/order")
    public Result cancelOrder(@RequestParam String outTradeNo);
}

在这里插入图片描述
     同样地,在 (七) 超时处理中,需要把逻辑改为,监听队列的 DelayMessageListener 的 delayMessage 方法,在监听到消息时,需要判断订单是否支付成功,如果没有支付成功,需要关闭订单、回滚库存。

进行测试,然后发现 order 和 pay 工程,循环依赖了😓😓😓。
     问题是这样的,order 工程中,监听订单的 DelayMessageListener 类用到了 WeiXinPayFeign :
在这里插入图片描述
     需要在 order 工程中去掉 changgou-service-pay 的依赖,换成 changgou-service-weixinpay-api 依赖,并在启动类上声明。(所以说,如果没有导入 feign 所在工程的依赖,而且启动类上也没声明 feign 所在包的话,导入 feign 调用的路径对应的代码的工程依赖就行啦???)
     这样 order 工程就没什么问题了。
    
     现在不循环依赖了,试着启动 order 工程,是没有问题的,再来启动 pay 工程,报了个错:
在这里插入图片描述
主要是在 pay 工程里,用了 order 的服务层:
在这里插入图片描述
    就是这个关闭订单的方法里,用到了逻辑删除订单的方法,会设置订单状态为支付失败,并回滚库存。
    这里,pay 的 controller 层里调用了 order 的服务层,又没有专门配置,所以报错了,解决过程见 https://blog.csdn.net/weixin_41750142/article/details/116277095。
    试着启动 pay 工程,报错:
在这里插入图片描述
    它说… … order 工程里的 CartServiceImpl 里 用到了 SkuFeign,但是找不到这样的 feign。奇了怪了… … order 工程运行起来都没有报错,为什么在 pay 工程里,并没有用到 CartService,反而报错了呢?(破案了,因为在启动类上写了 @ComponentScan(basePackages = {"com.changgou.order.service"}) ,这样就会扫描到这个包)
    还有 pay 的 controller 层里调用了 order 的服务层,毕竟也是不合理的,如果业务交叉了,可以考虑调用 feign,但是现在又没有现成的控制层逻辑可以用来进行逻辑删除,因为 orderService.deleteOrder 方法,是我直接写在服务层的… …本来就没打算让控制层调 😰。
    整个过程是这样的。在 order 工程中,一旦监听队列变化的方法,支付结果为支付失败时,就会 /cancel/order 的 feign,这个 feign 对应的是 pay 工程里的 cancelOrder 方法(为什么要放在 pay 工程里呢?是因为调用了微信支付提供的 “关闭订单” API ,在关闭订单后,才改变我们数据库里的订单状态,和回滚库存的),然后这个 pay 工程中的 cancelOrder 方法,改变订单状态和回滚库存,用的是 order 的服务层了。绕来绕去,order 用 feign 调 pay,然后 pay 的控制层 调 order 的服务层,结果又是 order 的另一个服务层报错了 🥶 。
    所以想要改 bug 很简单,不要在 pay 的控制层 调 order 的服务层!! 监听到队列的支付结果是支付失败时,调 /cancel/order 的 feign,但是 cancelOrder 方法里不要 改变订单状态和回滚库存,而是继续在监听队列的方法里,改变订单状态和回滚库存,这样就是 order 工程里调 order 的代码了。
原先:
在这里插入图片描述

@RequestMapping(value = "/cancel/order")
    public Result cancelOrder(@RequestParam String outTradeNo) throws Exception {
        Map resultMap = weixinPayService.cancelOrder(outTradeNo);
        if (resultMap.get("result_code").equals("FAIL")) {
            // 取消订单失败
            return new Result(true, StatusCode.ERROR, "取消订单失败", resultMap);
        } else {
            // 回滚库存
            orderService.deleteOrder(outTradeNo);
            return new Result(true, StatusCode.OK, "取消订单成功", resultMap);
        }
    }

这个方法是测试过的,没有问题。
修改为:
在这里插入图片描述
这样,pay 的 控制层不再调 order 的服务层了。
    
修改监听队列的方法:
在这里插入图片描述
     这样 pay 工程里也就不需要 order 依赖了,否则还牵扯认证的问题。

补充:发现原先的逻辑有误,原先的代码:

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

    @Autowired
    OrderService orderService;

    @Autowired
    WeiXinPayFeign weiXinPayFeign;

    /**
     * 支付结果监听
     */
    @RabbitHandler
    public void getMessage(String message) throws Exception {

        // 支付结果
        Map<String, String> resultMap = JSON.parseObject(message, Map.class);

        // 输出监听到的消息
        System.out.println(resultMap);

        // 通信标识
        String returnCode = resultMap.get("return_code");

        if (returnCode.equals("SUCCESS")) {
            // 业务结果
            String resultCode = resultMap.get("result_code");

            // 订单号
            String outTradeNo = resultMap.get("out_trade_no");

            // 支付成功
            if (resultCode.equals("SUCCESS")) {

                // 更改订单信息
                orderService.updateStatus(outTradeNo, resultMap.get("time_end"), resultMap.get("transaction_id"));


            } else {
                // 如果支付失败,需要关闭支付,取消订单,回滚库存
                weiXinPayFeign.cancelOrder(outTradeNo);
            }
        }
    }
}

    这还分支付成功和支付失败的情况呢是🥶🥶🥶,实际上,只有支付成功的时候才会进行结果通知回调的,支付失败的话根本就不会到这个方法里来,所以,需要修改为:

 @RabbitHandler
    public void getMessage(String message) throws Exception {

            // 支付结果
            Map<String, String> resultMap = JSON.parseObject(message, Map.class);

            // 输出监听到的消息
            System.out.println(resultMap);

            // 订单号
            String outTradeNo = resultMap.get("out_trade_no");
          
            // 更改订单信息
            orderService.updateStatus(outTradeNo, resultMap.get("time_end"), resultMap.get("transaction_id"));
        }

再有,原先的代码认为当 returnCode、resultCode 都为 SUCCESS 时,表示支付成功,需要修改支付状态为“已支付”,否则表示支付失败。然而,未支付的时候,returnCode、resultCode 也是为 SUCCESS 的: (returnCode、resultCode 只是说明可以正常返回响应结果,但不是说明支付成功)
在这里插入图片描述
在这里插入图片描述
所以如果要分支付成功和支付失败的情况的话,应该以交易状态作为判断依据:

       // 交易状态
        String tradeState=resultMap.get("trade_state");
        // 订单号
        String outTradeNo = resultMap.get("out_trade_no");

        if (tradeState.equals("SUCCESS")) {

                // 支付成功
                // 更改订单信息
                orderService.updateStatus(outTradeNo, resultMap.get("time_end"), resultMap.get("transaction_id"));


            } else {
                // 如果支付失败,需要关闭支付,取消订单,回滚库存
                weiXinPayFeign.cancelOrder(outTradeNo);
            }

九、总结

(1)导入微信支付的依赖,就可以使用 wxPayUtil 类 的一些方法,比如 获取随机字符串、Map 和 XML 的互相转换、通过密钥生成签名。
    
(2)使用封装了 HttpClient 的工具类访问微信支付服务器(其实直接用 WeiXinPay 类里的方法也行🤗)。
    
(3)调用 “统一下单" API,提供 支付订单号 out_trade_no 和 支付总金额 total_fee 参数,会得到用于支付扫描的二维码链接。
    
(4)调用 ”查询订单“ API,通过商品订单号进行查询。因为可能存在 像网络原因,导致我们本地服务器没有及时拿到,所以需要手动查询。
    
(5)至此,都是本地服务访问微信支付服务器,接下来,需要让微信支付服务器【外网】访问本地,使用内网穿透技术 NAT。提供外网域名、内网主机(IP地址)、内网端口,即可生成一个访问地址,通过这个访问地址,就可以让外网访问本地的服务了。
    
(6)在 pay 工程中提供获取支付结果的方法,同时,还应该让 order 工程也获取到,思路是使用消息队列,监听消息队列,当监听队列有支付成功的消息时,把订单状态改为支付成功。
    创建 名为 queue.order 队列,和 名为 exchange.order 的交换机,并进行绑定。
    交换机是生产者和队列之间的抽象,使得生产者与队列无直接联系,用于指定消息按什么规则,路由到哪个队列。生产者通过路由键 routingkey 将交换机和队列绑定起来。
    
    获取支付结果,调用 “支付结果通知” API,需要先将对应的路径(内网穿透地址)作为配置文件中 weixin:notifyurl 的属性值。这样,用户扫描付款码支付成功后,会调用到这个方法(这·就是回调?)并把支付结果发送给 queue.order 队列。
    OrderMessageListener 类实现对 queue.order 队列 的监听,通过消息的 “result_code” 参数判断是否支付成功。如果支付成功,需要修改订单状态,逻辑是 先通过商品订单号 查询到订单信息,设置订单支付时间为当前时间,设置支付状态为 “1” 已支付,设置事务 ID 为”支付结果通知“里的事务 ID,并把订单对应的记录更新到数据库 order 表中;如果支付失败,需要关闭订单,调用 ”关闭订单“ API,如果返回的结果中,result_code 为 SUCCESS,说明关闭订单成功,还需要回滚库存(因为之前下单的时候减少了库存),否则,说明可能遇到了 订单已支付、订单已关闭、系统错误等错误,只是将错误码放在响应结果中返回,并不会回滚库存。
    
(7)如果用户下单 30 分钟后还没有支付,也需要把订单取消。使用 Rabbit MQ 的 Time To Live、Dead Letter Exchange 属性,队列 orderDelayQueue 一过期,就把消息转发给队列 orderListenerQueue。在下单的 addOrder 方法中,添加逻辑:将订单消息发送给 orderDelayQueue 队列,并设置过期时间。DelayMessageListener 类实现对 orderListenerQueue 队列的监听。
    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值