paypal消息通知IPN

article/2025/9/13 20:47:26

paypal支付成功时会实时的把支付交易信息返回给我们,java会返回一个payment对象,里面有交易的信息包含付款人,订单费用,订单的收货地址,收款人,交易号等信息。我们拿到了这个payment就表示支付成功了,接下来就可以处理我们的逻辑了,比如修改订单状态,记录交易记录等操作了。

IPN其实就是paypal的交易状态的一种推送机制,是由paypal主动推送,只要我们配置好地址就可以了。

官方paypal对IPN的解释:IPN消息通知

消息地址设置

使用卖家账号登录paypal,然后找到用户信息-用户信息与设置,点进去


在点击销售工具,在里面找到即时付款通知项,点击更新



启用这个付款通知,并把自己的接口url填进去。

注意事项:

  1. 其实这个地址url可以是个restful的接口。
  2. 接口地址是https开头的,也就是要求使用SSL进行连接,其实Paypal Sandbox可以使用http,但是最后实际的Paypal接口,不支持http协议,paypal会校验是否是https地址。
  3. 在支付表单中,可以自己设置notify_url字段,来指定此次交易的信息应该发送到哪个地方,这样就可以覆盖在Profile中我们的设置,另外,这个字段要进行urlencode
  4. 我们得到的IPN信息中,status对应的便是交易状态,如Complete表示完成,首字母大写,而验证结果则是VERIFIEY或者INVALID,全部大写,具体的内容,可以查看Paypal官方的文档订单管理整合指南。

处理逻辑:

IPN的原理很简单,就是当产生了一个交易之后,交易状态发生变化时,如用户已经付款、或者退款、撤销时,Paypal利用常用的HTTP POST方式,将交易的一些变量提交给网站的某个页面(称之为IPN Handler),当这个页面接受到请求时候,将这些数据原封不动加上一个指示验证的cmd=_notify-validate,POST回Paypal的接口地址,如果数据正确,那么Paypal返回字符串VERIFIED,否则为INVALID,如果结果为VERIFIED,那么你的程序就可以使用这些数据进行操作。

通知传输的数据:

array ('act' => 'ipn','mc_gross' => '20.00','invoice' => '548531d624f59','protection_eligibility' => 'Eligible','address_status' => 'unconfirmed','item_number1' => '','tax' => '1.30','item_number2' => '','payer_id' => 'JARYJK2TES6C6','address_street' => 'NO 1 Nan Jin Road','payment_date' => '21:04:35 Dec 07, 2014 PST','payment_status' => 'Completed','charset' => 'gb2312','address_zip' => '200000','mc_shipping' => '1.20','mc_handling' => '0.00','first_name' => 'Test','mc_fee' => '0.98','address_country_code' => 'CN','address_name' => 'Buyer Test','notify_version' => '3.8','custom' => '','payer_status' => 'unverified','address_country' => 'China','num_cart_items' => '2','mc_handling1' => '0.00','mc_handling2' => '0.00','address_city' => 'Shanghai','verify_sign' => 'AomRS5l2W2xlt2An.GaSrAzpCl-NACIvh3Pz0HtrSBZzfcIeqDPGrXSk','payer_email' => 'yxw.2013.03-buyer@gmail.com','mc_shipping1' => '0.00','mc_shipping2' => '0.00','tax1' => '0.00','tax2' => '0.00','txn_id' => '5CS19517SJ894934R','payment_type' => 'instant','last_name' => 'Buyer','address_state' => 'Shanghai','item_name1' => 'Ground Coffee 40 oz','receiver_email' => 'yxw.2013.03@gmail.com','item_name2' => 'Granola bars','payment_fee' => '0.98','quantity1' => '1','quantity2' => '5','receiver_id' => '937CP9PSMDS2A','txn_type' => 'cart','mc_gross_1' => '7.50','mc_currency' => 'USD','mc_gross_2' => '10.00','residence_country' => 'CN','test_ipn' => '1','transaction_subject' => '','payment_gross' => '20.00','ipn_track_id' => 'a9059421a1dd7',
)

我们接收这些数据使用 cmd=_notify-validate 拼接并返回给paypal告诉paypal我们接收到了。后面我在做的过程中发现通知的数据有时候有parent_txn_id有时候又没有,后面去查文档才发现原来paypal第一次通知的时候没有parent_txn_id,后面交易状态改变后就会有这个parent_txn_id。其实这个parent_txn_id就是原始的交易号,第二次通知回调的时候parent_txn_id是原来的交易号,然后同时又会生成一个新的txn_id推送过来。

所以我这边做的逻辑判断是:如果parent_txn_id为空时则表示是paypal的第一次通知,这个时候我就去检查这个交易号之前实时支付成功的时候有没有存到,如果有则不管,没有则存起来。

parent_txn_id不为空的时候表示这是我们第二次或者多次接受到paypal通知了,这个时候我就去更新支付状态,订单状态。


    /**
     * 获取paypal的通知消息
     * @param request
     * @param response
     * @throws Exception
     */
    @ApiIgnore
    @RequestMapping(method = RequestMethod.POST, value = "/receivePaypalStatus")public void receivePaypalStatus(HttpServletRequest request, HttpServletResponse response) throws Exception {log.info("receivePaypalStatus >>>>>>>>>> 进入paypal后台支付通知");
        PrintWriter out = response.getWriter();
        try {/**
             * 获取paypal请求参数,并拼接验证参数
             */
            Enumeration<String> en = request.getParameterNames();
            String str = "cmd=_notify-validate";
            while (en.hasMoreElements()) {String paramName = en.nextElement();
                String paramValue = request.getParameter(paramName);
                //此处的编码一定要和自己的网站编码一致,不然会出现乱码,paypal回复的通知为‘INVALID’
                str = str + "&" + paramName + "=" + URLEncoder.encode(paramValue, "utf-8");
            }//建议在此将接受到的信息 str 记录到日志文件中以确认是否收到 IPN 信息
            log.info("=========================================================================================");
            log.info("paypal传递过来的交易信息:"+str);

            /**
             * 将信息 POST 回给 PayPal 进行验证
             */
            //验证地址测试环境和正式环境不一样配置在yml            URL u = new URL(paypalConfig.getWebscr());
            HttpURLConnection uc = (HttpURLConnection) u.openConnection();
            uc.setRequestMethod("POST");
            uc.setDoOutput(true);
            uc.setDoInput(true);
            uc.setUseCaches(false);
            //设置 HTTP 的头信息
            uc.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            PrintWriter pw = new PrintWriter(uc.getOutputStream());
            pw.println(str);
            pw.close();

            /**
             * 接受 PayPal IPN 回发的回复信息
             */
            BufferedReader in = new BufferedReader(new InputStreamReader(uc.getInputStream()));
            String res = in.readLine();
            in.close();

            /**
             * POST 信息分配给本地变量,可以根据您的需要添加
             */
            // 交易状态 Completed 代表交易成功
            String paymentStatus = request.getParameter("payment_status");
            // 交易时间
            String paymentDate = request.getParameter("payment_date");
            // 交易id
            String txnId = request.getParameter("txn_id");
            // 父交易id
            String parentTxnId = request.getParameter("parent_txn_id");
            // 收款人email
            String receiverEmail = request.getParameter("receiver_email");
            // 收款人id
            String receiverId = request.getParameter("receiver_id");
            // 付款人email
            String payerEmail = request.getParameter("payer_email");
            // 付款人id
            String payerId = request.getParameter("payer_id");
            // 交易金额
            String mcGross = request.getParameter("mc_gross");
            // 自定义字段,我们存放的订单ID
            String custom = request.getParameter("custom");


            if (res == null || res == "") {res = "0";
            }log.info("res = " + res);
            log.info("paymentStatus = " + paymentStatus);
            log.info("txnI = " + txnId);
            log.info("parentTxnId = " + parentTxnId);
            log.info("custom = " + custom);

            /**
             * 获取 PayPal 对回发信息的回复信息,判断刚才的通知是否为 PayPal 发出的
             */
            if (VERIFIED.equalsIgnoreCase(res)) {
                /**
                 * 如果 parentTxnId 不为空我们就认为是通知就不是第一次通知
                 */
                if (StringUtil.isNotEmpty(parentTxnId)) {// 根据父支付交易号查询付款表数据
                    List<ShopOrderPayment> list = shopOrderPaymentService.getOrderPaymentByParentTxnId(parentTxnId);
                    if (null != list && list.size() > 0) {ShopOrderPayment shopOrderPayment = list.get(0);

                        /**
                         * 更新支付状态
                         */
                        ShopOrderPayment updateOrderPayment = new ShopOrderPayment();
                        updateOrderPayment.setId(shopOrderPayment.getId());
                        updateOrderPayment.setPaymentStatus(paymentStatus);
                        updateOrderPayment.setTransactionId(txnId);
                        shopOrderPaymentService.updateOrderPaymentById(shopOrderPayment);

                        /**
                         * 保存支付历史记录数据
                         */
                        ShopOrderPaymentHistory paymentHistory = new ShopOrderPaymentHistory();
                        BeanUtils.copyProperties(shopOrderPayment, paymentHistory);
                        paymentHistory.setPaymentStatus(paymentStatus);
                        paymentHistory.setTransactionId(txnId);
                        shopOrderPaymentHistoryService.savePaymentHistory(paymentHistory);

                        /**
                         * 判断状态是complete则更新订单状态为待确认收货
                         * 如果是refunded则更新订单状态为已完成
                         */
                        if (COMPLETED_STATUS.equalsIgnoreCase(paymentStatus)) {paypalService.updateOrderStatus(shopOrderPayment.getOrderId(), com.sunvalley.shop.order.constants.Constants.OrderStatus.STATUS_PRE_REC);
                        } else if (REFUNDED_STATUS.equalsIgnoreCase(paymentStatus)) {paypalService.updateOrderStatus(shopOrderPayment.getOrderId(), com.sunvalley.shop.order.constants.Constants.OrderStatus.STATUS_COMPLETE);
                        }} else {log.error("父支付交易号:" + parentTxnId + " 在支付表中不存在");
                        log.error("Class: "+this.getClass().getName()+" method: "+Thread.currentThread().getStackTrace()[1].getMethodName());
                    }} else {/**
                     * 第一次回调通知,根据 txnId 查询。如果存在则表示支付实时返回结果已经记录了,不存在则表示实时返回结果没有记录到
                     */
                    List<ShopOrderPayment> paymentList = shopOrderPaymentService.getOrderPaymentByTxnId(txnId);
                    if (null != paymentList && paymentList.size() > 0) {ShopOrderPayment orderPaymentTmp = paymentList.get(0);
                        if (COMPLETED_STATUS.equalsIgnoreCase(orderPaymentTmp.getPaymentStatus())) {log.info("================ 支付表数据已经是complete了,不需要更新 ================");
                        } else {// 如果回传的状态不是complete则更新我们的支付数据
                            /**
                             * 更新支付状态
                             */
                            ShopOrderPayment updateOrderPayment = new ShopOrderPayment();
                            updateOrderPayment.setId(orderPaymentTmp.getId());
                            updateOrderPayment.setPaymentStatus(paymentStatus);
                            shopOrderPaymentService.updateOrderPaymentById(orderPaymentTmp);

                            /**
                             * 保存支付历史记录数据
                             */
                            ShopOrderPaymentHistory paymentHistory = new ShopOrderPaymentHistory();
                            BeanUtils.copyProperties(orderPaymentTmp, paymentHistory);
                            paymentHistory.setPaymentStatus(paymentStatus);
                            paymentHistory.setTransactionId(txnId);
                            shopOrderPaymentHistoryService.savePaymentHistory(paymentHistory);
                        }} else {/**
                         * 保存支付信息
                         */
                        ShopOrderPayment orderPayment = new ShopOrderPayment();
                        // redis中获取订单ID
                        if (StringUtil.isNotEmpty(custom)) {orderPayment.setOrderId(Integer.valueOf(custom));
                        } else {log.info("***************** paypal回传的订单ID为空 **************");
                        }// 订单总价
                        orderPayment.setAmountPaid(new BigDecimal(mcGross));
                        // 交易号
                        orderPayment.setTransactionId(txnId);
                        // 支付成功时把交易号同时写进父交易号中
                        orderPayment.setParentTransationId(txnId);
                        // 收款方ID
                        orderPayment.setReceiveId(receiverId);
                        // 收款方邮箱
                        orderPayment.setReceiveEmial(receiverEmail);
                        // 收款方状态
                        orderPayment.setPaymentStatus(paymentStatus.toLowerCase());
                        // 付款方ID
                        orderPayment.setPayerId(payerId);
                        // 付款方邮箱
                        orderPayment.setPayerEmail(payerEmail);

                        // 保存支付数据
                        ShopOrderPayment orderPay = paypalService.savePayment(orderPayment);

                        log.info("================= 支付信息保存成功,订单状态更新成待发货成功 ====================");
                    }}} else if (INVALID.equalsIgnoreCase(res)) {//非法信息,可以将此记录到您的日志文件中以备调查
                log.error("paypal完成支付发送IPN通知返回状态非法,请联系管理员,请求参数:" + str);
                log.error("Class: "+this.getClass().getName()+" method: "+Thread.currentThread().getStackTrace()[1].getMethodName());
                out.println("confirmError");
            } else {//处理其他错误
                log.error("paypal完成支付发送IPN通知发生其他异常,请联系管理员,请求参数:" + str);
                log.error("Class: "+this.getClass().getName()+" method: "+Thread.currentThread().getStackTrace()[1].getMethodName());
                out.println("confirmError");
            }} catch (Exception e) {log.error("确认付款信息发生IO异常" + e.getMessage());
            log.error("Class: "+this.getClass().getName()+" method: "+Thread.currentThread().getStackTrace()[1].getMethodName());
            out.println("confirmError");
        }out.flush();
        out.close();
    }
以上是我个人的代码,可以收到paypal的消息通知,仅供参考,转载需注明,谢谢~






http://chatgpt.dhexx.cn/article/nKJCAMXq.shtml

相关文章

paypal资料

什么是即时付款通知IPN 当您收到新的付款交易或者已发生的付款交易的状态发生变化时&#xff0c;PayPal都将异步&#xff08;即不作为网站付款流程的一部分&#xff09; 发送付款详细数据到您所指定的URL&#xff0c;以便您了解买家付款的具体情况并做出相应的响应。这个过程我…

css 上下布局 flex,Css Flex布局

Flex布局是Css3中新加入的额外布局系统。 传统布局基于盒模型,依赖“display”、“position”、“float”属性,对于特殊布局非常不便。 因此2009年,W3C提出新的布局方案-Flex布局,但由于浏览器兼容问题,Flex布局并没有大范围铺开。 实现Flex布局的条件 1.必须有一个父级容…

html flex 上中下布局,flex 布局

FlexiableBox即是弹性盒,用来进行弹性布局,一般跟rem(rem伸缩布局(转))连起来用比较方便,flexbox负责处理页面布局,然后rem处理一些flex顾及不到的地方(rem伸缩布局主要处理尺寸的适配问题),布局还是要传统布局的。 布局的传统解决方案,基于盒状模型,依赖display属性 +p…

详细讲解flex布局

一、flex布局基本概念 在没有使用flex布局之前&#xff0c;常用布局有&#xff1a;流式布局&#xff0c;浮动布局&#xff0c;定位布局等等。这些布局的缺陷是子元素需要自己控制自己在父元素中的位置&#xff0c;还要注意父元素高度坍塌。 flex布局是一种布局模型&#xff0…

CSS常用布局二(flex布局)

flex布局 前言&#xff1a;flex是flexible box的缩写&#xff0c;译为“弹性布局”&#xff0c;用来为盒模型提供最大的灵活性&#xff0c;任何一个容器都可以指定为flex布局&#xff0c;只需要设置“display:flex"即可&#xff1b;行内元素可以通过设置”display:inline…

flex布局(详解)

目录 前言 一、何为Flex布局 二、基本概念 三、容器的属性 3.1 flex-direction属性 3.2 flex-wrap属性 3.3 flex-flow 3.4 justify-content属性 3.5 align-items属性 3.6 align-content属性 四、项目的属性 4.1 order属性 4.2 flex-grow属性 4.3 flex-shrink属性 …

Flex布局详解

Flex 布局详解 一、入门 1. flex 是什么&#xff1f; flex 是 Flexible Box 的缩写&#xff0c;就是弹性盒子布局的意思 2. 为什么我们需要 flex? 解决元素居中问题 自动弹性伸缩&#xff0c;合适适配不同大小的屏幕&#xff0c;和移动端 3.flex 常见术语 三个2 序号简…

SQL语句的解析过程

于最近需要做一些sql query性能提升的研究&#xff0c;因此研究了一下sql语句的解决过程。在园子里看了下&#xff0c;大家写了很多相关的文章&#xff0c;大家的侧重点各有不同。本文是我在看了各种资料后手机总结的&#xff0c;会详细的&#xff0c;一步一步地讲述一个sql语句…

SQL学习TASK06

section A 1.创建员工信息表&#xff1a; CREATE TABLE Employee (s_product_id char(4) NOT NULL, s_name VARCHAR(32) NOT NULL, s_salary INTEGER, s_department_id INTEGER); 创建部门信息表&#xff1a; CREATE TABLE department (d_id char(4) NOT NULL, d_name VARCHAR…

MySQL高级SQL语句

目录 一、常用查询 1、按关键字排序 1.1 前期准备 1.2 升序、降序列出数据 1.3 找出其中南京的数据并以分数降序列出 1.4 查询学生信息先按兴趣id降序排列&#xff0c;相同分数的&#xff0c;id也按降序排列 1.5 查询学生信息先按兴趣id降序排列&#xff0c;兴趣id相同的…

HANA 一些sql语句

函数&#xff01;&#xff01; 时间函数&#xff1a;DAYS_BETWEEN、ADD_DAYS、FORMAT、CURRENT_DATE、YEAR、MONTH等。 字符串函数&#xff1a;CONCAT、TRIM、LENGTH、REPLACE、STRING_AGG、SUBSTRING等&#xff1b; 数字函数&#xff1a; ROUND、FLOOR、RAND、ABS等 视图&…

SQL语句的封装

本篇供个人学习使用&#xff0c;有问题欢迎讨论 封装SQL语句 ​ 在封装SQL语句之前&#xff0c;我们得知道什么是DAO封装与实体类以及JDBC工具类的封装与连接数据库的具体流程。 ​ 关于JDBC工具类的封装可以查看我的另一篇博文 JDBC工具类的封装 ​ 想了解具体的连接数据库…

mysql sql delete语句_SQL Delete语句

在本教程中,您将学习如何使用SQL DELETE语句删除表中的一行或多行。 1. SQL DELETE语句简介 要从表中删除一行或多行,请使用DELETE语句。 DELETE语句的一般语法如下: DELETE FROM table_name WHERE condition; 首先,提供要删除行的表名称(table_name)。 其次,在WHERE子句中…

MySQL入门(5)——基于datagrip的SQL语句学习

目录 一、什么是SQL二、SQL约束1、主键约束&#xff08;1&#xff09;添加主键约束方式一&#xff1a;创建表时&#xff0c;在字段描述处&#xff0c;声明指定字段为主键方式二&#xff1a;创建表时&#xff0c;在constraint约束区域&#xff0c;声明指定字段为主键方式三&…

MySQL数据库增删改查及聚合查询SQL语句学习汇总

目录 数据库增删改查SQL语句 MySQL数据库指令 1.查询数据库 2.创建数据库 3.删除数据库 4.选择数据库 创建表table 查看所有表 创建表 查看指定表的结构 删除表 数据库命令进行注释 增删改查&#xff08;CRUD&#xff09;详细说明 增加 SQL库提供了关于时间的…

第一部分_SQL查询语句学习

第一部分 SQL查询语句的学习 单表查询 查询订购日期在1996年7月1日至1996年7月15日之间的订单的订购日期、订单ID、客户ID和雇员ID等字段的值 SELECT 订购日期,订单ID,客户ID,雇员ID FROM 订单 WHERE 订购日期 BETWEEN 1996-07-01 AND 1996-07-15 查询供应商的ID、公司名称、…

了解RS-232、RS-485串口通信协议

文章目录 一、串口通信协议1、RS-232标准2、RS-485标准3、RS232、485电平与TTL电平的区别4、"USB/TTL转232"模块工作原理1、工作流程2、驱动 2、总结3、参考资料 一、串口通信协议 对于通信协议&#xff0c;我们可以分为两个层面进行理解&#xff0c;分别是物理层和…

RS485通讯协议

https://blog.csdn.net/qq_29344757/article/details/71516037 1. 硬件层协议 通讯协议主要是实现两个设备之间的数据交换功能&#xff0c;通讯协议分硬件层协议和软件层协议。硬件层协议决定数据如何传输问题&#xff0c;比如要在设备1向设备2发送0x63&#xff0c;0x63的二进…

RS——485通讯协议

1、RS485通讯实验简介 RS485是一种工业控制环境中 常用的通讯协议&#xff0c;它具有抗干扰能力强、传输距离远的特点。485协议又232协议改进而来&#xff0c;协议层不变&#xff0c;只改进了物理层&#xff0c;因而保留了串口通讯协议应用简单的特点。 看图就知道了&#xf…

485Modbus协议

1.RS485 1&#xff09;485通信 --差分传输 物理层&#xff1a; 通信引脚 A B 使用双绞线通信。 发送器&#xff1a; 逻辑1&#xff1a; A>B AB之间电压为2V~6V 逻辑0&#xff1a; A<B AB之间电压为-2V~-6V 接收器&#xff1a; 逻辑1&#xff1a; A>B AB之间电…