一、搭建pay-consumer环境

MAVEN依赖

其他的依赖与前面的consumer项目相类似,主要增加了支付宝支付接口的依赖:alipay-sdk-java

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.fall</groupId>
  4. <artifactId>crowdfunding17-member-api</artifactId>
  5. <version>1.0-SNAPSHOT</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework.cloud</groupId>
  9. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.springframework.boot</groupId>
  13. <artifactId>spring-boot-starter-web</artifactId>
  14. </dependency>
  15. <dependency>
  16. <groupId>org.springframework.boot</groupId>
  17. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.springframework.boot</groupId>
  21. <artifactId>spring-boot-configuration-processor</artifactId>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-data-redis</artifactId>
  26. </dependency>
  27. <dependency>
  28. <groupId>org.springframework.session</groupId>
  29. <artifactId>spring-session-data-redis</artifactId>
  30. </dependency>
  31. <!-- 阿里支付接口依赖 -->
  32. <dependency>
  33. <groupId>com.alipay.sdk</groupId>
  34. <artifactId>alipay-sdk-java</artifactId>
  35. <version>4.10.124.ALL</version>
  36. </dependency>
  37. <!-- 测试 -->
  38. <dependency>
  39. <groupId>org.springframework.boot</groupId>
  40. <artifactId>spring-boot-starter-test</artifactId>
  41. <scope>test</scope>
  42. <exclusions>
  43. <exclusion>
  44. <groupId>org.junit.vintage</groupId>
  45. <artifactId>junit-vintage-engine</artifactId>
  46. </exclusion>
  47. </exclusions>
  48. </dependency>
  49. </dependencies>

支付宝接口需要的配置类

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "ali.pay")
public class PayProperties {
    private String appId;
    private String merchantPrivateKey;
    private String aliPayPublicKey;
    private String notifyUrl;
    private String returnUrl;
    private String signType;
    private String charset;
    private String gatewayUrl;
}

application.yml

server:
  port: 7000
spring:
  application:
    name: crowd-pay
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
  redis:
    host: 192.168.0.101
  session:
    store-type: redis
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1000/eureka/
# 在配置文件中设置支付宝公钥等信息
ali:
  pay:
    ali-pay-public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqfvJJ7MdtFp+OEO6NbBo0q9VDkh996CU7f2uy8cVl3sbss+CxiRJCcdRO3kqI8tQUneRk8QdBqatbQnBttIEBxPa0pCFQ0CSw3dGOp/r8hs/K7gWQ2OrBQaGHXTGqWzJPrIRK8IEzQpCz1O6Ta4VaPLIvnFnszfzGbbQFAXnNCIMnlOr3vUWeABAkX2PfVhMAHLUUecudC3Dm2rX8Df3QBceVJl8pWw0A7GH4MsbaxAtAOmSoFLDCNoxybmCPnvnpwQh2sE+TwiETDXG9iUcUr9TON7Oqy+i0CD0EWgmt3uhCbXftRsV2FZypZ8/8lTViy0cB6Uy8KBPgUOCMrN0swIDAQAB
    app-id: 2021000116694364
    charset: utf-8
    gateway-url: https://openapi.alipaydev.com/gateway.do
    merchant-private-key: MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCnNtznnSJKvG5m6YTviQOoB0qF8pi+/sowJ8JjotKcRZ0A2kk6DTkbssmKgFrv4WYYTgd7k8DJFQEHGXTTA+F6EOBBRwDXXIKB2YoKoJmH8LrRWFhRMyUwzkKPlWAIw9uDWGX74ihF6kFpQg9xN8Z/ouhSsjkLA5/ECv1q7420cQ5gPuqgOPEIz9xE9/1Mo/cKSvOZ0BDJ5A+ZjHvkBsK8gqYyydPb69CLxGkGMU6JHK8f4mIEVJaBE4V9KzBJszl4mg5mW1h1sEXcK6LKNPkALbazDu4ni2E1q9TuQ7e4BuUdwpXVDdEswC/5hUV77jarFTa20PYkIE1N5sC2iP4RAgMBAAECggEARNU+xlFW5nD++XozcO90A91MfitJWYZE8OHNEXrbO90kLk+tSSpcUdydPlhsFscwYdztE5/VAnGVBLkj8CFu1fLQfc6euAiq6I0GWC2YRe+A1+CIKpTlQFwH5ldVOwZ0/HKNOMz2SyGNX6kD9lVBbxU+kZwwAyqwv+DORRDrwlUEjX9xwIebRHqjSuL+QgOZSCCVufs/MIn4xYRmxGbL/ta+CzK2vQiTtJOqeOiAEDL1U/jKyd7+vOi/2UqvopZrsYjylg7zDqVzQrUrsYYPkanBp0FKwWDN4653lgnADXEimBBHjmZ0hEWdo+DWOn6Ve8S0fKmcUaPTsbumGDpC4QKBgQDl8gw+XLRlE+jwbbgyC7QVDP/xNaVpdhK17CTTH1snQOEvZAQnFqRF6D0mGSQ+79v8pYB4jWMV2Gr8fHFIdqKMwHpAbc5M11st2r2Q0IICtcCxGf6/XaVDehRGiiT8gV+EpBa80HjNSV0svzQUt7m2h9YPpJVf54YDALdXKzppRQKBgQC6KTFBszsaJGfrdzwQNjJVOqHukopoW+kZTrECF5J+wMf2g5xDuji2X4QBk+tKAfJvREE+LZMnsFl2vsVlz2baYbp1aHPBaqsV/gw2sSp1d8C0SOOtolVCwU0XgWQdlhqh66W9gv9N/flq37DNg/5MYVLGHc8FXM7Fm8f2ZznAXQKBgDow0+pgOYKu1C8sBQYdMhh1rruTEOYsbDW/qd4HvGClqZb9n5Noxm+ugvMGP8bxmmFIXMelOCMKHqEAuQZWh3JJL2boouGNSlTDW1+vdIdNCZOH+ZDiGPWHimQDh7Ki5tJBWEO2GiwRWdP+Gyb3C7dmjsRsqSSZCa/u2XbkyKZlAoGAWvXBoY6HtFBRjpXl/S3eDhLmCIIFqtQt4zW1+4W1CMX9bpAEDHuzNjkZcQPrlW+6G2/ISS9PlbbvH7w7C3H5mX7i8nclCD6sdcR6B0ctrdPjjrOGu9JzwnhcyL1qQ6KSRMe1ZhHZ2SvdcMD3zJNP79Yl8bOi8cXVKmzhmZ36c+kCgYADngv9iZhfU1xm6LvlkwHofv+LuUqhlrpnhb8sFEkhC0PHXOtm9Yx3tRiu9fYZ21RD7pwCODn+rC9qsNJrKlIJcYqg1Kg5QS6Ki7cOk1ORTW8v8xpvuVRx4Z/oy+LyatLEMiSfghzoKZ1QyydoOKM0UHJpVwKKJvk1bP+iXRfZWg==
    notify-url: http://8bwgvf.natappfree.cc/pay/notify
    return-url: http://localhost/pay/return
    sign-type: RSA2

二、“立即付款”跳转

1、前端页面

order-confirm-order.html

<!--按钮标签-->
<li style="margin-top:10px;">
    <button id="payBtn" disabled="disabled" type="button" class="btn btn-warning btn-lg">
        <i class="glyphicon glyphicon-credit-card"></i> 
        立即付款
    </button>
</li>

<!--添加一个初始无内容的表单,用于提交付款请求时携带数据-->
<form id="summaryForm" action="/pay/generate/order" method="post"></form>

<!--id=payBtn对应的按钮的单击响应函数-->
<script>
    // 支付按钮的单击响应事件
    $("#payBtn").click(function () {
        // 收集要提交给表单的数据
        var addressId = $("[name=addressId]:checked").val();
        var invoice = $("[name=invoiceRadio]:checked").val();
        var invoiceTitle = $.trim($("[name=invoiceTitle]").val());
        var remark = $.trim($("[name=remark]").val());
        // 提交表单
        $("#summaryForm")
            .append("<input type='hidden' name='addressId' value='"+ addressId +"' />")
            .append("<input type='hidden' name='invoice' value='"+ invoice +"' />")
            .append("<input type='hidden' name='invoiceTitle' value='"+ invoiceTitle +"' />")
            .append("<input type='hidden' name='orderRemark' value='"+ remark +"' />")
            .submit();
    });
</script>

2、后端handler方法

@Controller
public class PayHandler {

    @Autowired
    private PayProperties payProperties;

    @Autowired
    private MySQLRemoteService mySQLRemoteService;

    private Logger logger = LoggerFactory.getLogger(PayHandler.class);

    // 通过ResponseBody注解,让当前方法的返回值成为响应体,在浏览器上显示支付宝的支付界面
    @ResponseBody
    @RequestMapping("generate/order")
    public String generateOrder(OrderVO orderVO, HttpSession session) throws UnsupportedEncodingException, AlipayApiException {

        // 得到session域中的orderProjectVO
        OrderProjectVO orderProjectVO = (OrderProjectVO)session.getAttribute("orderProjectVO");

        // 将orderProjectVO赋给前端传来的orderVO
        orderVO.setOrderProjectVO(orderProjectVO);

        // 生成支付宝订单号
        // 使用uuid生成用户id部分
        String uuid = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();

        // 根据日期生成字符串
        String time = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());

        // 组装
        String orderNum = time + uuid;

        // 存入orderVO
        orderVO.setOrderNum(orderNum);

        // 计算订单金额
        Double orderAmount = (double)(orderProjectVO.getReturnCount() * orderProjectVO.getSupportPrice() + orderProjectVO.getFreight());

        // 存入orderVO
        orderVO.setOrderAmount(orderAmount);

        // 把orderVO存入session域
        session.setAttribute("orderVO", orderVO);

        return sendRequestToAliPay(orderNum,orderAmount,orderProjectVO.getProjectName(),orderProjectVO.getReturnContent());
    }

    /**
     *
     * @param orderNum 订单号
     * @param orderAmount 总金额
     * @param subject 订单名称,这里用项目名称
     * @param body 商品描述,这里用回报的描述
     * @return 返回页面
     * @throws AlipayApiException
     * @throws UnsupportedEncodingException
     */
    private String sendRequestToAliPay(String orderNum, Double orderAmount, String subject, String body) throws AlipayApiException, UnsupportedEncodingException {

        //获得初始化的AlipayClient
        AlipayClient alipayClient = new DefaultAlipayClient(
                payProperties.getGatewayUrl(),
                payProperties.getAppId(),
                payProperties.getMerchantPrivateKey(),
                "json",
                payProperties.getCharset(),
                payProperties.getAliPayPublicKey(),
                payProperties.getSignType());

        //设置请求参数
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(payProperties.getReturnUrl());
        alipayRequest.setNotifyUrl(payProperties.getNotifyUrl());


        alipayRequest.setBizContent("{\"out_trade_no\":\""+ orderNum +"\","
                + "\"total_amount\":\""+ orderAmount +"\","
                + "\"subject\":\""+ subject +"\","
                + "\"body\":\""+ body +"\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");

        // 返回
        return alipayClient.pageExecute(alipayRequest).getBody();
    }

    // return请求对应方法
    @ResponseBody
    @RequestMapping("/return")
    public String returnUrlMethod(HttpServletRequest request,HttpSession session) throws UnsupportedEncodingException, AlipayApiException {
        //获取支付宝GET过来反馈信息
        Map<String,String> params = new HashMap<>();
        Map<String,String[]> requestParams = request.getParameterMap();
        for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
            String name = iter.next();
            String[] values = 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,
                payProperties.getAliPayPublicKey(),
                payProperties.getCharset(),
                payProperties.getSignType()); //调用SDK验证签名

        //——请在这里编写您的程序(以下代码仅作参考)——
        if(signVerified) {
            //商户订单号
            String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8");

            //支付宝交易号
            String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8");

            //付款金额
            String total_amount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"),"UTF-8");

            // 得到session域中的orderVO
            OrderVO orderVO = (OrderVO)session.getAttribute("orderVO");

            // 给orderVO设置支付宝交易号
            orderVO.setPayOrderNum(trade_no);

            // 存入mysql数据库
            ResultEntity<String> resultEntity = mySQLRemoteService.saveOrderRemote(orderVO);

            logger.info("save order result: " + resultEntity.getResult());

            return "trade_no:"+trade_no+"<br/>out_trade_no:"+out_trade_no+"<br/>total_amount:"+total_amount;
        }else {
            return "验签失败";
        }
    }



    // notify请求对应方法
    @RequestMapping("/notify")
    public void notifyUrlMethod(HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
        //获取支付宝POST过来反馈信息
        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,
                payProperties.getAliPayPublicKey(),
                payProperties.getCharset(),
                payProperties.getSignType()); //调用SDK验证签名

        if(signVerified) {//验证成功
            //商户订单号
            String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8");

            //支付宝交易号
            String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8");

            //交易状态
            String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8");

            if(trade_status.equals("TRADE_FINISHED")){
                //判断该笔订单是否在商户网站中已经做过处理
                //如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
                //如果有做过处理,不执行商户的业务程序

                //注意:
                //退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知
            }else if (trade_status.equals("TRADE_SUCCESS")){
                //判断该笔订单是否在商户网站中已经做过处理
                //如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
                //如果有做过处理,不执行商户的业务程序

                //注意:
                //付款完成后,支付宝系统发送该交易状态通知
            }

            logger.info("success");
            logger.info("out_trade_no" + out_trade_no);
            logger.info("trade_no" + trade_no);
            logger.info("trade_status" + trade_status);

        }else {//验证失败
            logger.info("fail");

            //调试用,写文本函数记录程序运行情况是否正常
            //String sWord = AlipaySignature.getSignCheckContentV1(params);
            //AlipayConfig.logResult(sWord);
        }
    }
}

3、将订单存入数据库

① 修改OrderVO类的代码

将原本为Integer类型的addressId改为String类型,使其与OrderPO对象对应,否则会出现无法存入addressId的情况。

(记得让OrderVO实现序列化接口,因为需要存入session,存入redis)

② 远程api接口

@RequestMapping("save/order/remote")
ResultEntity<String> saveOrderRemote(@RequestBody OrderVO orderVO);

③ mysql模块

handler:

@RequestMapping("save/order/remote")
ResultEntity<String> saveOrderRemote(@RequestBody OrderVO orderVO) {
    try {
        orderService.saveOrder(orderVO);
        return ResultEntity.successWithoutData();
    } catch (Exception e) {
        e.printStackTrace();
        return ResultEntity.failed(e.getMessage());
    }
}

service:

@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
@Override
public void saveOrder(OrderVO orderVO) {
    // 创建OrderPO对象
    OrderPO orderPO = new OrderPO();
    // 从传入的OrderVO给OrderPO赋值
    BeanUtils.copyProperties(orderVO,orderPO);
    // 将OrderPO存入数据库
    orderPOMapper.insert(orderPO);
    // 得到存入后自增产生的order id
    Integer orderId = orderPO.getId();
    // 得到orderProjectVO
    OrderProjectVO orderProjectVO = orderVO.getOrderProjectVO();
    // 创建OrderProjectPO对象
    OrderProjectPO orderProjectPO = new OrderProjectPO();
    // 赋值
    BeanUtils.copyProperties(orderProjectVO,orderProjectPO);
    // 给orderProjectPO设置orderId
    orderProjectPO.setOrderId(orderId);
    // 存入数据库
    orderProjectPOMapper.insert(orderProjectPO);
}

这里因为需要在存入OrderPO后得到自增的orderId,需要修改MyBatis的代码:

即增加 useGeneratedKeys=”true” keyProperty=”id”

<insert id="insert" parameterType="org.fall.entity.po.OrderPO" useGeneratedKeys="true" keyProperty="id" >
  insert into t_order (id, order_num, pay_order_num, 
    order_amount, invoice, invoice_title, 
    order_remark, address_id)
  values (#{id,jdbcType=INTEGER}, #{orderNum,jdbcType=CHAR}, #{payOrderNum,jdbcType=CHAR}, 
    #{orderAmount,jdbcType=DOUBLE}, #{invoice,jdbcType=INTEGER}, #{invoiceTitle,jdbcType=CHAR}, 
    #{orderRemark,jdbcType=CHAR}, #{addressId,jdbcType=CHAR})
</insert>

此时支付功能就基本完成了。

三、注意点

此处使用的是支付宝支付的沙箱环境,具体在支付宝开放平台查看,如果需要使用正规环境,则必须提供企业信息等。

另外,想要使用支付功能,必须对外有公网地址,而不能仅仅使用内网地址,这样外部接口是无法调用到我们本地的代码的,因此这里我使用了natapphttps://natapp.cn/),进行内网穿透,打开软件后使用它提供的域名就可以访问我们内网的80端口了(需要在自己的natapp账号等地方设置好)