0


支付宝搭配rabbitmq实现支付通知

支付宝搭配rabbitmq实现支付通知

前言:本篇文章是基于springcloud,需要搭建nacos与feign和gateway,rabbitmq环境,如果不了解不建议学习,另外作用与参考,不适用与复制

主要有支付的下单,查询账单,关闭订单,打印账单 ,记录支付日志(因为是电力业务没有退单)

1,接入准备

开发平台注册:https://open.alipay.com/

步骤1:首先登录注册

在这里插入图片描述

步骤2:填写个人信息选择自研服务最后同意加入,然后成功入驻了

常规开发接入流程(没有可以使用沙箱):

创建应⽤:选择应⽤类型、填写应⽤基本信息、添加应⽤功能、配置应⽤环境(获取⽀付宝公

钥、应⽤公钥、应⽤私钥、⽀付宝⽹关地址,配置接⼝内容加密⽅式)、查看 APPID

绑定应⽤:将开发者账号中的APPID和商家账号PID进⾏绑定

配置秘钥:即创建应⽤中的“配置应⽤环境”步骤

上线应⽤:将应⽤提交审核

签约功能:在商家中⼼上传营业执照、已备案⽹站信息等,提交审核进⾏签约

沙箱版(推荐使用)

沙箱环境配置:https://opendocs.alipay.com/common/02kkv7
沙箱版⽀付宝的下载和登录:https://open.alipay.com/platform/appDaily.htm?tab=tool

支付参数配置

获取参数需要在https://opendocs.alipay.com/common/02kkv7参考文档配置

alipay.app-id=9021000143230(用户id)

alipay.seller-id=20887210712(商家id)

alipay.gateway-url=https://openapi-sandbox.dl.alipaydev.com/gateway.do (支付宝网关地址)

alipay.merchant-private-key= ( 设置应用私钥)

alipay.alipay-public-key= ( 设置支付宝公钥)

alipay.content-key=NP156rcE9fG5oi+Oo6LUIw== (接口内容加密方式 )

alipay.return-url= http://localhost:5173/#/elect/index (返回地址,主要是前端支付提交后,返回给浏览器的地址)

alipay.notify-url=http://q22u4y.natappfree.cc/system/client/trade/notify (回调地址(内网穿透),在文章后面提供文档配置)

启动内⽹穿透ngrok或者natapp 本文采用的是natapp,开启后需要把forwarrding的http://q22u4y.natappfree.cc地址替换

下载安装地址: Natapp 安装及配置教程(Windows)【安装】_natapp教程-CSDN博客

( 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常
访问,每次网络断开需要重新替换)

在这里插入图片描述

配置参数获取流程:

首先进入https://open.alipay.com/develop/sandbox/account

点击左上角的沙箱应用,找到接口加签方式,点击查看获取私钥与公钥,用户id与商家id都在该界面

在这里插入图片描述
在这里插入图片描述

引入配置文件:

在config包下引入支付配置文件:

package com.igeek.config;

import com.alipay.api.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

import javax.annotation.Resource;

/**
 * ClassName: AlipayClientConfig
 * Description:
 *
 * @Author 刘洋
 * @Create 2024/9/13 16:17
 * @Version 1.0
 */
@Configuration
@PropertySource("classpath:application.properties下的可以不配置该注解")//如果是application.properties下的可以不配置该注解
public class AlipayClientConfig {
    //自动获取application.properties中的配置
    @Resource
    private Environment config;
    @Bean
    public AlipayClient alipayClient() throws AlipayApiException {
        AlipayConfig alipayConfig = new AlipayConfig();
//设置网关地址
        alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
//设置应用Id
        alipayConfig.setAppId(config.getProperty("alipay.app-id"));
//设置应用私钥
        alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
//设置请求格式,固定值json
                alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
//设置字符集
        alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);
//设置支付宝公钥
        alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
//设置签名类型
                alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
//构造client
        AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
        return alipayClient;
    }
}

config:引入延迟队列配置文件(注意:这里存在死信队列,如果业务不需要可以删除,笔者没有删除是留着未来开发使用)

package com.igeek.config;

import org.apache.xmlbeans.impl.xb.xsdschema.Public;
import org.checkerframework.checker.units.qual.A;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * ClassName: AckConfig
 * Description:
 *
 * @Author 刘洋
 * @Create 2024/9/13 21:43
 * @Version 1.0
 */
@Configuration
public class AckConfig {
    public static final String ACK_EXCHANGE = "ack_exchange";
    public static final String ACK_QUEUE = "ack_queue";
    public static final String ACK_ROUTING_KEY = "ack";

    //死信队列
    public static final String DEAD_LETTER_EXCHANGE = "dead_letter_exchange";
    public static final String DEAD_LETTER_QUEUE = "dead_letter_queue";
    public static final String DEAD_LETTER_ROUTING_KEY = "dead_letter";

    //失败策略
    public static final String FAIL_EXCHANGE = "fail_exchange";
    public static final String FAIL_QUEUE = "fail_queue";
    public static final String FAIL_ROUTING_KEY = "fail";
    @Bean
    public DirectExchange ackExchange()
    {
        return ExchangeBuilder.directExchange(ACK_EXCHANGE).build();

    }
    @Bean
    public Queue ackque()
    {
        return QueueBuilder.durable(ACK_QUEUE).deadLetterRoutingKey(DEAD_LETTER_ROUTING_KEY).deadLetterExchange(DEAD_LETTER_EXCHANGE).build();

    }

@Bean
public Binding bindAck()
{
    return  BindingBuilder.bind(ackExchange()).to(ackExchange()).with(ACK_ROUTING_KEY);
}
@Bean
public  Binding bindDeadLetter()
{
    return BindingBuilder.bind(deadQueue()).to(directExchange()).with(DEAD_LETTER_ROUTING_KEY);
}
@Bean
    public  DirectExchange directExchange()
{
    return ExchangeBuilder.directExchange(DEAD_LETTER_EXCHANGE).build();
}
@Bean
    public Queue deadQueue()
{
    return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
}
@Bean
    public DirectExchange failExchange()
{
    return  ExchangeBuilder.directExchange(FAIL_EXCHANGE).build();
}

    @Bean
    public Queue FailQueue()
    {
        return QueueBuilder.durable(FAIL_QUEUE).build();
    }
    @Bean
    public Binding faillBidig()
    {
        return BindingBuilder.bind(FailQueue()).to(failExchange()).with(FAIL_ROUTING_KEY);
    }
}

支付配置

Controller包下:

 @GetMapping("/trade/page/pay/{prouctId}") 
    //下单接口
    public Result tradePage(@PathVariable Long prouctId) throws AlipayApiException {
        String form=aliPayService.tradeCreade(prouctId);  调用service方法会返回一个表单,通过这个表单通过前端渲染出现支付界面
        return   Result.ok(form);
    }

serviceimpl包下:

  @Override
    @Transactional
    public String tradeCreade(Long prouctId) throws AlipayApiException {
        log.info("{}",prouctId);
        Bill byBillingId = feignBill.findByBillingId(prouctId);通过远程调用获取用户id,笔者这里由于项目分离,订单与支付模块不在一起,如果在一起直接调用订单service接口查找订单即可
        log.info("{}",byBillingId);
        AlipayTradePagePayRequest alipayTradePagePayRequest = new AlipayTradePagePayRequest();
        调用支付宝的支付请求接口

//
        alipayTradePagePayRequest.setReturnUrl(environment.getProperty("alipay.return-url"));
        //配置返回地址
        alipayTradePagePayRequest.setNotifyUrl(environment.getProperty("alipay.notify-url"));
        配置回调地址
        JSONObject jsonObject=new JSONObject();
        jsonObject.put("out_trade_no",byBillingId.getId());
        jsonObject.put("total_amount",byBillingId.getPrice());
        jsonObject.put("subject",byBillingId.getClientId());
        jsonObject.put("product_code", "FAST_INSTANT_TRADE_PAY");
        alipayTradePagePayRequest.setBizContent(jsonObject.toString());;
        AlipayTradePagePayResponse alipayTradePagePayResponse = alipayClient.pageExecute(alipayTradePagePayRequest);//执行
        if (alipayTradePagePayResponse.isSuccess())
        {
            System.out.println("调用成功");
        }
        else {
            System.out.println("调用失败");
        }
        log.info("{}",alipayTradePagePayResponse.getBody());
        rabbitTemplate.convertAndSend(MQConstant.DELAYED_EXCHANGE,MQConstant.DELAYED_ROUTING_KEY,byBillingId.getId(),message -> {message.getMessageProperties().setDelay(1*1000*60); return  message;});//5*1000*60
        //使用rabbitmq延迟队列设置延迟时间1分钟
return  alipayTradePagePayResponse.getBody();
    }

前端页面:主要使用的是vue3+ts+vbenadmin模板(如果前端框架不一样需要修改,在后面几篇文章我会出vbenadmin的搭建,配置)

  async function handleEdit(record: Recordable) {
    console.log(record.id);
    
    await findOrderByid(record.id).then((res) => {
      console.log(res);
      document.write(res);
    });
  }

定义:

export function findOrderByid(params: string, mode: ErrorMessageMode = 'modal') {
  return defHttp.get(
    {
      url: Api.payElect + params,
    },
    {
      errorMessageMode: mode,
    },
  );
}

在这里插入图片描述

异步通知:

controller包:主要是进行信息检验,如果信息没有被修改,就把订单插入数据库

  @PostMapping("/trade/notify")
    public String tradeNotify(@RequestParam Map<String,String> params) throws AlipayApiException {
        log.info("正在支付中");
        String result="failure";

        log.info("{}",params);
        boolean sign = AlipaySignature.rsaCheckV1(params, environment.getProperty("alipay.alipay-public-key"), AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2);//支付参数检验
        if (!sign)
        {
            log.error("验签失败");
            return  result;
        }
       String result1="success";
      log.warn("验签成功");
        Bill outTradeNo = feignBill.findByBillingId(Long.valueOf(params.get("out_trade_no")));
        同前文一样,由于订单与支付controller不在一个模块采用feign进行模块接口调用,主要是查询账单id是否在数据库存在
        if (outTradeNo==null)  //订单编号检验
        {
               log.warn("编号不存在");

               return result;
        }
        String  price= params.get("total_amount");
        int  prices = new BigDecimal(price).multiply(new BigDecimal("100")).intValue();
        int i = outTradeNo.getPrice().multiply(new BigDecimal("100")).intValue();
        if (prices!=i) //订单价格检验,由于时间紧迫,上述判断都没有使用规范的stringutils类似的判断
         {
//             log.info("{}",outTradeNo.getPrice());
//             log.info("{}",params.get("total_amount"));
//             log.info("{}",prices);
//             log.info("{}",i);
             log.warn("金额被修改");
             return result;
         }
         if (!environment.getProperty("alipay.app-id").equals(params.get("app_id")))//用户id
         {
             log.warn("用户id不对");
             return result;
         }
        if (!environment.getProperty("alipay.seller-id").equals(params.get("seller_id")))//商户id判断
        {
            log.warn("商户id不对");
            return result;
        }
        aliPayService.update(params,outTradeNo);//接口在下文
        //保证接口幂等性,防止接口重复调用

        CorrelationData correlationData = new CorrelationData(params.get("out_trade_no"));//把订单编号存入该类//确保信息唯一性

        rabbitTemplate.convertAndSend(AckConfig.ACK_EXCHANGE,AckConfig.ACK_ROUTING_KEY,params,correlationData);
        return result;
    }

serviceimpl:采用了reentrantlock数据锁,防止并发带来的问题

    
    需要定义:private final ReentrantLock lock = new ReentrantLock();
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update(Map<String, String> params,Bill bill1) {
        if (lock.tryLock()) {

            try {
                if (bill1.getStatus() == "1") {//幂等性    防止多次调用
                    log.info("重复调用");
                    return;
                }
                Bill bill = bill1.setStatus("1");
                Boolean isflag = feignBill.updateByBillingId(bill);//更新订单状态
                if (isflag) {  
                    log.info("订单状态修改成功");
                } else {
                    log.warn("订单修改失败");
                }
                //插入支付记录
                Payment payment = new Payment();
                payment.setBillingId(bill.getId());
                payment.setPaymentMoney(bill.getPrice());

                paymentService.insert(payment);记录支付日志
            }
            finally {
                lock.unlock();释放
            }

        }
    }

rabbitmq:

listener配置:

延迟队列监听:

package com.igeek.linsten;

import com.alibaba.fastjson.JSONObject;
import com.igeek.FeignBill;
import com.igeek.constant.MQConstant;
import com.igeek.entity.Bill;
import com.igeek.entity.Payment;
import com.igeek.service.AliPayService;
import io.lettuce.core.dynamic.annotation.Key;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.A;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.HashMap;

/**
 * ClassName: DelayedConsumerListener
 * Description:
 *
 * @Author 刘洋
 * @Create 2024/9/19 21:14
 * @Version 1.0
 */
@Component
@Slf4j
public class DelayedConsumerListener {
    @Autowired
    private AliPayService aliPayService;
@Autowired
private FeignBill feignBill;

    @RabbitListener(bindings = @QueueBinding(value =@Queue(value = MQConstant.DELAYED_QUEUE,durable = "true"),exchange =@Exchange(value =MQConstant.DELAYED_EXCHANGE,type = "direct", delayed = "true"),key = MQConstant.DELAYED_ROUTING_KEY))
    public void receive(String msg)
    {

            log.info("{}", msg + LocalDateTime.now());
            String queue = aliPayService.queue(Long.parseLong(msg));
            log.info("{}", queue);
            HashMap<String,JSONObject> hashMap = JSONObject.parseObject(queue, HashMap.class);
            log.info("{}", hashMap);
        JSONObject jsonObject = hashMap.get("alipay_trade_query_response");
        HashMap hashMap1 = JSONObject.parseObject(jsonObject.toJSONString(), HashMap.class);
        String s =jsonObject.getString("trade_status");
            if (!s.equals("TRADE_SUCCESS")) {
                log.info("订单没有支付");
                aliPayService.cancel(Long.parseLong(msg));
            }
            if (s.equals("TRADE_SUCCESS")) {//如果订单支付,网络断了,那么商户就不找到是否支付,因此使用延迟队列对订单排查
                log.info("订单已经支付");
                Bill byBillingId = feignBill.findByBillingId(Long.parseLong(msg));
                log.info("{}",byBillingId.getStatus());
                if (!byBillingId.getStatus().equals("1"))
                {
                    log.info("{}",byBillingId.getStatus());
                    aliPayService.update(hashMap1,byBillingId);
                }

            }
        }

    }

重试机制配置:

package com.igeek.linsten;

import com.alibaba.fastjson.JSONObject;
import com.alipay.api.internal.util.AlipaySignature;
import com.igeek.config.AckConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * ClassName: RetryConsumer
 * Description:
 *
 * @Author 刘洋
 * @Create 2024/9/13 22:10
 * @Version 1.0
 */
@Component
@Slf4j
public class RetryConsumer {
AtomicInteger count=new AtomicInteger();
@RabbitListener(queues = AckConfig.ACK_QUEUE,ackMode = "AUTO")
public void receive(Message message)
{
    log.info("{}", (Object) message.getMessageProperties().getHeader("spring_returned_message_correlation"));
    //通过编号id查询手机号调用发送接口信息发送邮件
    //发送邮件
}
@RabbitListener(queues = AckConfig.DEAD_LETTER_QUEUE)
    public void receiveDeadLetter(String message){
        log.warn("监听死信队列中的信息:{}" , message);
//通过
        //重置重试计数器
        count.set(0);
    }

}

取消订单

controller包下:

       @ApiOperation("用户取消订单")
       @PostMapping("/trade/close/{orderNo}")
    public  Result cancel(@PathVariable Long orderNo)
       {
           aliPayService.cancel(orderNo);
           return Result.ok("订单已经撤销");
       }

serciceimpl下:

    @Override
    @Transactional
    public void cancel(Long orderNo) {
        this.closeBill(orderNo);
        Bill Billing = feignBill.findByBillingId(orderNo);
        feignBill.updateByBillingId(Billing.setStatus("2"));
    }
      //方法采用支付宝提供的关闭订单方法
      private void closeBill(Long orderNo) {

       try {
           AlipayTradeCloseRequest alipayTradeCloseRequest = new AlipayTradeCloseRequest();
           JSONObject jsonObject = new JSONObject();
           jsonObject.put("out_trade_no",orderNo);

           alipayTradeCloseRequest.setBizContent(jsonObject.toString());
            AlipayTradeCloseResponse execute = alipayClient.execute(alipayTradeCloseRequest);
            if (execute.isSuccess())
            {
                log.info("订单撤销成功");
            }
            else
            {
                log.info("撤销失败");
            }
        } catch (AlipayApiException e) {
            throw new RuntimeException(e);
        }

    }

查询订单:

controller:

       @ApiOperation("查询订单")
       @PostMapping("/trade/query/{orderNo}")
    public  Result queue(@PathVariable Long orderNo)
    {
        aliPayService.queue(orderNo);
        return Result.ok("订单已经撤销");
    }

serviceimpl:

@Override
    public String queue(Long orderNo) {

        try {
            AlipayTradeQueryRequest Request = new AlipayTradeQueryRequest();
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("out_trade_no",orderNo);
            Request.setBizContent(jsonObject.toString());
            AlipayTradeQueryResponse execute = alipayClient.execute(Request);
            if (execute.isSuccess())
            {
                log.info("调用成功",execute.getBody());
                return  execute.getBody();
            }
            else
            {
                log.info("调用失败{}",execute.getBody());
                return  null;
            }
        } catch (AlipayApiException e) {
            throw new RuntimeException(e);
        }

    }

定时如果电表欠费每天早上生成订单

task包下:

最后效果如下:

开始支付页面
在这里插入图片描述

下单页面:

在这里插入图片描述

支付成功回退页面:

在这里插入图片描述
在这里插入图片描述

可以查看第3个图片的第一条数据,结果已经支付

在这里插入图片描述

后端打印信息:可以查看到订单没有出现2次修改
在这里插入图片描述

模拟订单支付成功,但是网断了没有支付记录,那么rabbitmq是否会通过延迟队列重新插入支付记录,那么我可以通过支付成功后改变订单状态把已支付改为未支付观察是否重新调用

开始页面,处理第一条数据
在这里插入图片描述

进行支付:
在这里插入图片描述

支付成功后面,第一个status 0变成1 1表示支付 0表示未支付

手动改为0 模拟订单支付但是后端网络出现问题,

在这里插入图片描述

第一条订单状态改为0

在这里插入图片描述

一分钟后,可以查看到订单重新修改订单状态,并插入支付记录

在这里插入图片描述

可以查看到订单状态已经被修改
在这里插入图片描述

生成账单测试:

访问订单生成地址得到压缩包,存储的是订单信息

在这里插入图片描述

最后,由于篇幅有限以及笔者经验有限,可能存在文章内容不全,可以在文章下面留言,笔者看到后会及时回复
标签: rabbitmq ruby 分布式

本文转载自: https://blog.csdn.net/m0_69440360/article/details/142415363
版权归原作者 金银程序员1号 所有, 如有侵权,请联系我们删除。

“支付宝搭配rabbitmq实现支付通知”的评论:

还没有评论