谷粒商城二十二订单服务支付宝支付

我们支付暂时只开发支付宝,官方文档在蚂蚁金服开放平台-电脑网站支付

按照正规的流程,我们的系统要接入支付宝,肯定是需要大量的审核过程,而且需要我们的项目上线。

那现在我们就想测试该怎么办?支付宝为我们提供了沙箱环境,我们可以在应用未上线之前来进行调试。

我们所需要的所有代码都在支付宝官方的demo中可以直接复制粘贴。

加密算法

我们在调用支付宝支付,网络间传输是需要加密的,防止有人拦截做一些修改的话,就很危险。

对称加密

加密和解密用的是同一把密钥。

不安全,只要有一方的密钥泄露,密文就会被解密
谷粒商城二十二订单服务支付宝支付_第1张图片

非对称加密

加密和解密用的密钥不一样

如果发送方密钥泄露,但是他不能对密文解密,相对安全
如果接收方密钥泄露,密文就会被解密

发送方加密用a,接收方解密用b
接收方发送用c,发送方接收用d

我们用4把密钥进行加解密,除非完全知道这四把钥匙,否则就不能模拟完整的通信过程。

所以我们就使用非对称加密做整个金融级别的数据加密,rsa是现在非常常用的金融领域的非对称加密算法。
谷粒商城二十二订单服务支付宝支付_第2张图片

公钥私钥

公钥和私钥是一个相对概念
它们的公私性是相对于生成者来说的。
一对密钥生成后,保存在生成者手里的就是私钥,
生成者发布出去大家用的就是公钥

签名、验签

商户利用商户的私钥加密数据传给支付宝时需要商户提供商户的公钥(支付宝利用公钥可以解密)

支付宝亦然

例如下订单,我们可以把传递的参数order=123&money=100进行加密成密文传给支付宝,如果别人把拦截到这段密文,将密文发给支付宝,也是相对不安全。

我们现在把参数order=123&money=100进行明文传递,给我们将要发送的数据,做上一个唯一签名

唯一签名:用商户私钥对传递的参数加密
参数和唯一签名是一个整体,任意一方发生改变都不能验签成功。
验签:支付宝使用公钥验证签名

通信过程中万一有人拦截,修改参数的明文,而我们验签失败,则认为被人篡改。
例如支付宝给我们返回的是成功,有人篡改成失败,那么我们就一直支付,就会造成损失。

我们有了这四把钥匙,我们整个通信就是可靠的,尤其是支付宝私钥,它的管理是非常严格的。

谷粒商城二十二订单服务支付宝支付_第3张图片

内网穿透

这个目前来看是测试阶段需要的,大概就是我们把商城项目部署到我们自己电脑(或虚拟机)上,别人通过外网肯定是不能访问到我们本地的项目的。

我们要解决的就是外网也可以访问我们本地的项目

在支付这个场景下,支付成功后,支付宝会异步通知我们订单是否成功,此时支付宝需要访问到我们的项目,如果我们部署到本地的话,显然是不行的,就需要用到内网穿透了。
谷粒商城二十二订单服务支付宝支付_第4张图片
这个想表达的意思就是外网是不能访问局域网的,但是局域网是可以访问外网的(前提肯定是得联网)

谷粒商城二十二订单服务支付宝支付_第5张图片
如果我们想让外界(京东或其他不在局域网的pc)访问我们的局域网,除了走正规流程外(注册域名,分配ip),我们测试期间还可以做内网穿透

我们去内网穿透的服务商,服务商让我们去下载一个软件,比如我的电脑下载了一个服务商软件,我们的软件一启动,我们的软件和内网穿透服务商就会建立连接,内网穿透服务商还会给我的电脑分配一个域名(随机,可能很长,不需要备案,是内网穿透服务商的二级或者三级域名,内网穿透的一级域名备案后,它的二三级域名就无需备案了),别人想访问我们就访问这个域名即可,就实现了别人可以访问我们的电脑。

比如我们的电脑部署了商城网站,别人通过这个域名可以直接访问到我们本地电脑的商城网站。

同理,其他的电脑也可以这样。

简介

内网穿透功能可以允许我们使用外网的网址来访问主机;
正常的外网需要访问我们项目的流程是:

  1. 买服务器并且有公网固定 IP
  2. 买域名映射到服务器的 IP
  3. 域名需要进行备案和审核

使用场景

  1. 开发测试(微信、支付宝)
  2. 智慧互联
  3. 远程控制
  4. 私有云

内网穿透的常用软件

  1. natapp:https://natapp.cn/ 优惠码:022B93FD(9 折)[仅限第一次使用]
  2. 续断:www.zhexi.tech 优惠码:SBQMEA(95 折)[仅限第一次使用]
  3. 花生壳:https://console.hsk.oray.com/forward

我用的是花生壳,内网穿透的功能免费赠送一年

idea设置utf8

因为支付宝支付期间整个使用的编码方式都是utf8,所以我们的idea也设置为utf8,
file-editor-file encodings,将能看到的所有编码全部设置为utf8。

code

阿里支付工具类

都是从demo中抽取过来的,所需要的所有信息都可以从支付宝沙箱环境中获取,例如商户私钥,支付宝公钥等信息

package com.atlinxi.gulimall.order.config;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.atlinxi.gulimall.order.vo.PayVo;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;


// 支付宝的各种配置可以迁移到配置文件中
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {

    //在支付宝创建的应用的id
    private   String app_id = "2021077403";

    // 商户私钥,您的PKCS8格式RSA2私钥
    private  String merchant_private_key = "MIIEvQIBAy1Ag/tNEjsnxkzZn6ZUJYeG4MkN51KzLj1VRMGhPZj4jjnj4ljqlgBs+ah8vKFbC0ebebFYYP86gmQJkYLntlDycxA2F7pGlVkPJwuYKciFT0sFtr7iM4b4VDiQ89vxu7Zxh2HnQk/fJFjUQHxSU1Vf/P7kL9DERZHsvZzI9KALs00EkECanRm97A9Fx1+aceLdZ1mIO/06EQLIHHW3znuoA5fOHqU=";
    // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
    private  String alipay_public_key = "MIIBIjANBgkqhib7PGEDpx6dBu9PpcFja7HNia5Xh6OgBmhekOu5D/bi5FpyWmrstjVxhQfCmHTkRYJABkbpGBz1WB1E4OKhzS0sHokOgPoQGUEPTSEMwIDAQAB";
    // 服务器[异步通知]页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    // 支付成功后,每隔一段时间发送一条消息,告知支付成功,接收到消息后,我们的后端修改订单信息
    // 一般25小时通知8次,通知的间隔频率是(支付成功,4m,10m,10m,1h,2h,6h,15h)
    private  String notify_url = "https://j53k36.yicp.fun/payed/notify";

    // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    //同步通知,支付成功,一般跳转到成功页
    private  String return_url = "http://member.gulimall.com/memberOrder.html";

    // 签名方式
    private  String sign_type = "RSA2";

    // 字符编码格式
    private  String charset = "utf-8";

    // 支付宝网关; https://openapi.alipaydev.com/gateway.do
    private  String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";

    public  String pay(PayVo vo) throws AlipayApiException {

        //AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
        //1、根据支付宝的配置生成一个支付客户端
        AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
                app_id, merchant_private_key, "json",
                charset, alipay_public_key, sign_type);

        //2、创建一个支付请求 //设置请求参数
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(return_url);
        alipayRequest.setNotifyUrl(notify_url);

        //商户订单号,商户网站订单系统中唯一订单号,必填
        String out_trade_no = vo.getOut_trade_no();
        //付款金额,必填
        String total_amount = vo.getTotal_amount();
        //订单名称,必填
        String subject = vo.getSubject();
        //商品描述,可空
        String body = vo.getBody();

        alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
                + "\"total_amount\":\""+ total_amount +"\","
                + "\"subject\":\""+ subject +"\","
                + "\"body\":\""+ body +"\","
                + "\"timeout_express\":\"1m\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");

        String result = alipayClient.pageExecute(alipayRequest).getBody();

        //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
        System.out.println("支付宝的响应:"+result);

        return result;

    }
}

html

<a th:href="'http://order.gulimall.com/payOrder?orderSn=' + ${submitOrderResp.order.orderSn}">支付宝a>

支付且支付宝同步通知

package com.atlinxi.gulimall.order.web;

import com.alipay.api.AlipayApiException;
import com.atlinxi.gulimall.order.config.AlipayTemplate;
import com.atlinxi.gulimall.order.service.OrderService;
import com.atlinxi.gulimall.order.vo.PayVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class PayWebController {


    @Autowired
    AlipayTemplate alipayTemplate;

    @Autowired
    OrderService orderService;


    /**
	 * 
     * 1. 将支付页让浏览器展示。
     * 2. 支付成功以后,我们要跳到用户的订单列表页
     * @param orderSn
     * @return
     * @throws AlipayApiException
     */
    // produces,告知返回的是一个html
    @ResponseBody
    @GetMapping(value = "/payOrder",produces = "text/html")
    public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {

//        PayVo payVo = new PayVo();
//        payVo.setBody(); // 订单的备注
//        payVo.setOut_trade_no(); // 订单号
//        payVo.setSubject(); // 订单的主题,名称
//        payVo.setTotal_amount();

        PayVo payVo = orderService.getOrderPay(orderSn);

        // 支付宝给我们返回的是一个支付页面,将此页面直接交给浏览器就行
        // 这个页面就是需要我们输账号密码的那个页面
        String pay = alipayTemplate.pay(payVo);

        System.out.println(pay);

        return pay;

    }
}








@Override
    public PayVo getOrderPay(String orderSn) {

        PayVo payVo = new PayVo();

        OrderEntity order = this.getOrderByOrderSn(orderSn);

        List<OrderItemEntity> order_sn = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
        OrderItemEntity entity = order_sn.get(0);

        // 数据库金额字段是四位小数
        // setScale() 保留两位小数,ROUND_UP,只要省去两位小数不为0,则往上进1
        payVo.setTotal_amount(order.getPayAmount().setScale(2,BigDecimal.ROUND_UP).toString());
        payVo.setOut_trade_no(order.getOrderSn());
        // 数据库里没有设置这个字段,我们暂时拿订单的第一个购物项的名字来作为支付的主题
        payVo.setSubject(entity.getSkuName());
        payVo.setBody("这是备注");

        return payVo;
    }

支付宝返回的支付页面

表单里有订单的所有信息,并且会自动提交,就显示出了我们的支付页面

<form name="punchout_form" method="post" action="https://openapi.alipaydev.com/gateway.do?charset=utf-8&method=alipay.trade.page.pay&sign=CJrpUozUzOJSoEuugPhHY9zaqV64j7vPGCUzT2B5Iy7a%2B1YWB3VI%2BUOfp4Q0XZHDqT%2FQiX6sywRvk2GQFSM5UNzRwZFP3JHGmkT9sPLc7aNQqKGv4HtjLBi1rx%2FnDdQRAEVNCVgbtKIHl6euEoNJmU6%2FiENbRC68cBIHrbPtrDHpk5Hi44mk54OZftfZzL7X7K89bcaYWvGrCW%2F1K%2FJwH43DS6YHBsc%2BhuHVout3Uk3Ea00A3IXOKj%2BIWoHcNY1XUtDsCAf3fSZKODLHn3inNdQnTZyzha86RHEFzdClfYNg5v42P7Tbyq8l7%2FLA1QWMjH4J%2FqMzcfxhCOjtM1DUMQ%3D%3D&return_url=https%3A%2F%2Fj53k272236.yicp.fun%2Falipay.trade.page.pay-JAVA-UTF-8%2Freturn_url.jsp¬ify_url=https%3A%2F%2Fj53k272236.yicp.fun%2Falipay.trade.page.pay-JAVA-UTF-8%2Fnotify_url.jsp&version=1.0&app_id=2021000122677403&sign_type=RSA2×tamp=2023-04-19+16%3A05%3A11&alipay_sdk=alipay-sdk-java-4.35.107.ALL&format=json">
<input type="hidden" name="biz_content" value="{"out_trade_no":"202304191604304891648598432704532481","total_amount":"34998.00","subject":"华为mate50 黑色 12 直屏旗舰 超光变XMAGE影像 北斗卫星消息 低电量应急模式","body":"这是备注","product_code":"FAST_INSTANT_TRADE_PAY"}">
<input type="submit" value="立即支付" style="display:none" >
</form>
<script>document.forms[0].submit();</script>

订单列表页需要的数据

package com.atlinxi.gulimall.member.web;

import com.atlinxi.common.utils.R;
import com.atlinxi.gulimall.member.feign.OrderFeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.HashMap;
import java.util.Map;

@Controller
public class MemberWebController {


    @Autowired
    OrderFeignService orderFeignService;

    @GetMapping("/memberOrder.html")
    public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") String pageNum, Model model){

        // 获取到支付宝给我们传来的所有请求数据,
        // request,验证签名,如果正确可以去修改

        // 查出当前登录用户的所有订单列表数据
        Map<String,Object> page = new HashMap<>();
        page.put("page",pageNum);
        R r = orderFeignService.listWithItem(page);

        model.addAttribute("orders",r);


        return "orderList";
    }
}

支付宝异步回调 验签 修改订单信息

因为支付宝异步通知访问我们的服务器,所以我们的服务器需要能被公网访问。

我是在花生壳直接创建了一条映射到本地的订单服务,它好像不能像老师那样配置域名,就是少了nginx的配置,但异步通知的效果是达到了。

老师在内网穿透软件上配置的是域名,order.gulimall.com,经过nginx转发,此时利用外网的域名去请求nginx转发的话,它找不到我们之前配置的host(*.gulimall.com),就跳转到了nginx默认指定的页面。

就需要以下配置,$host是自动获取请求的host,这里我们需要手动指定,请求路径为/payed的主机地址就是order.gulimall.com

新增监听的那个主机地址的话,不加它会去找静态资源,而它又是没有配置静态资源的,所以会报错,加上就不报了,至于为什么,也不是很明白,老师说不写域名的话就自动去找localhost了。todo
谷粒商城二十二订单服务支付宝支付_第6张图片

package com.atlinxi.gulimall.order.listener;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
import com.alipay.api.internal.util.AlipaySignature;
import com.atlinxi.gulimall.order.config.AlipayTemplate;
import com.atlinxi.gulimall.order.service.OrderService;
import com.atlinxi.gulimall.order.vo.PayAsyncVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

@RestController
public class OrderPayedListener {



    @Autowired
    AlipayTemplate alipayTemplate;


    @Autowired
    OrderService orderService;

    /**
     * 支付宝会传来很多的请求参数,会自动帮我们封装成vo数据
     * @param vo
     * @param request
     * @return
     */
    @PostMapping("/payed/notify")
    public String handleAlipayed(PayAsyncVo vo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {

        // 只要我们收到了支付宝给我们异步的通知,告诉我们订单支付成功。返回success
        // 支付宝就再也不通知了

        // 验签 验证是否是支付宝给我们返回的数据
        // 万一别人给我们发请求,告诉我们支付成功了,但是实际上是假的,那就出问题了

		// 这段也是直接在demo中粘贴过来的
        Map<String,String> params = new HashMap<String,String>();
        Map<String,String[]> requestParams = request.getParameterMap();
        for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
            String name = (String) iter.next();
            String[] values = (String[]) requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            //乱码解决,这段代码在出现乱码时使用
//            valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
            params.put(name, valueStr);
        }

        boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(), alipayTemplate.getCharset(), alipayTemplate.getSign_type());


        if (signVerified){

            System.out.println("签名验证成功。。。");



            String result = orderService.handlePayResult(vo);


            return result;
        }else {

            System.out.println("签名验证失败。。。");
            return "error";
        }


    }
}









 /**
     * 处理支付宝的支付结果
     * @param vo
     * @return
     */
    @Override
    public String handlePayResult(PayAsyncVo vo) {
        // 1. 保存交易流水
        PaymentInfoEntity infoEntity = new PaymentInfoEntity();

        infoEntity.setAlipayTradeNo(vo.getTrade_no());
        infoEntity.setOrderSn(vo.getOut_trade_no());
        infoEntity.setPaymentStatus(vo.getTrade_status());
        infoEntity.setCallbackTime(vo.getNotify_time());

        paymentInfoService.save(infoEntity);


        // 2. 修改订单的状态信息

        // 前者可退款,后者不可退款
        if (vo.getTrade_status().equals("TRADE_SUCCESS") || vo.getTrade_status().equals("TRADE_FINISHED")){
            // 支付成功状态
            String outTradeNo = vo.getOut_trade_no();

            this.baseMapper.updateOrderPayedStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());
        }



        return "success";
    }

用户登录拦截器

package com.atlinxi.gulimall.order.interceptor;

import com.atlinxi.common.constant.AuthServerConstant;
import com.atlinxi.common.vo.MemberRespVo;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class LoginUserInterceptor implements HandlerInterceptor {


    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //

        //

        /**
         * 在库存服务解锁库存的时候需要远程调用订单服务查询订单状态,以此来判断该订单是否需要解锁库存,
         *      因为这是服务之间的请求,没有携带session,进入订单服务的时候会因为没有获取到登录信息而被拦截
         *      而我们在库存服务的时候已经判断了是否登录,所以这里是不需要判断登录的,放行就是了
         *
         * uri 就是类似于上面那个路径,url是完整的请求地址
         *
         * order/order/status/{orderSn}  orderSn是动态的
         *
         *
         */
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match = antPathMatcher.match("/order/order/status/**", request.getRequestURI());
        boolean match1 = antPathMatcher.match("/payed/notify", request.getRequestURI());

        if (match || match1){
            return true;
        }


        MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);

        if (attribute!=null){

            loginUser.set(attribute);

            return true;
        }else {
            // 没登录就去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return  false;
        }

    }
}

收单

  1. 订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态改为已支付了,但是库存解锁了。

    使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
    就是设置一个超时时间,过期支付宝支付页面就失效了,阿里支付工具类中设置timeout_express即可

  2. 由于时延等问题。订单解锁完成,正在解锁库存的时候,异步通知才到

    订单解锁,手动调用收单

  3. 网络阻塞问题,订单支付成功的异步通知一直不到达

    查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝此订单的状态

  4. 其他各种问题

    每天晚上闲时下载支付宝对账单,一 一进行对账

苏迎澜和小逸也是类似的相处模式,他们会像朋友一样讨论各种各样的问题。做项目遇到困难的时候,小逸看到妈妈头疼的样子,问“你在烦恼什么?”苏迎澜如实讲出来,小逸常常给出一些天马行空的答案,“你可以…”

https://baijiahao.baidu.com/s?id=1760481532554271247

一个妈妈的反校园暴力“战斗”

你可能感兴趣的:(谷粒商城,java)