掘金 后端 ( ) • 2024-05-02 10:09

theme: channing-cyan

前言

在电商系统中,支付环节是非常重要的一个环节,对于做过电商项目的同学如果没有做过支付功能,那都不好意思说自己做过电商项目。然而对于个人开发者想要打通支付环节,却有着复杂的商业流程和短时间难以具备的资质。无论是微信支付、支付宝支付还是银联支付,都有自己通过备案审核的网站或App,还需要提供营业执照。即便是走服务商模式也需要具备通过备案审核的网站或App或已上线的小程序。

这一套复杂和麻烦的流程估计就劝退了很多个人开发者了,但是有个非常好的消息就是支付宝给广大个人开发者提供了沙箱环境,个人仅需一个支付宝账号,就可以在沙箱环境中实现支付宝支付。而在实现沙箱支付的基础上再实现正式支付那就简单多了,只需要我们在支付宝开发平台创建对应的应用,然后走完支付宝签约流程并修改与支付宝支付相关的配置变量即可完成正式上线工作。

本文主要先带领大家实现支付宝的沙箱支付功能,后面等笔者的阿里云服务器申请完域名备案并通过YunGouOS平台开通服务商模式签约后再来继续演示真实的线上支付。

沙箱环境介绍

这里简单介绍下沙箱环境,它是支付宝开放平台为开发者提供的安全低门槛的测试环境,开发者在沙箱环境中调用接口无需具备所需的商业资质,无需绑定和开通产品,即可实现支付功能。合理使用沙箱环境,可以让研发流程与商业流程并行,加速项目的交付。沙箱环境的支付用法与生产环境基本一致,仅需修改少量配置即可。

alipay01.png

图 1 有沙箱和无沙箱场景支付功能开发流程

接入准备

1)首先我们需要访问支付宝沙箱应用的开发者控制台,使用支付宝账号登录即可,访问地址:

https://open.alipay.com/develop/sandbox/app

sandboxPay01.png

图 2 沙箱应用网页/移动应用信息数据

2)点击沙箱应用->接口内容加密方式右边的设置按钮,生成公私秘钥

生成公私秘钥后就可以点击公钥模式后面的【查看】按钮在打开的界面中看到应用公钥应用私钥支付公钥

三段长字符串,点击复制公钥和复制私钥接口拷贝到项目中的配置文件中

publicPriviteKey.png

图 3 应用公钥、应用私钥和支付宝公钥数据

支付流程

网页端可以通过调用支付接口alipay.trade.page.pay(统一收单下单并支付页面接口)来实现支付,具体调用可以参考下面的时序图

alipay05.png

图 4 支付宝支付过程时序图

这里值得注意的是支付是否成功可以通过两种方式来得知:

  • 通过支付宝的异步通知来获取交易的支付结果
  • 过调用支付宝的alipay.trade.query(统一收单线下交易查询接口)查询交易的支付结果。

支付实现

开通内网穿透服务

提交支付订单后,阿里支付结果的通知接口必须是一个公网可访问的接口,但在开发环境中我们一般不会部署到云服务器中,而是在本地调试。而公网要想访问到内网服务必须通过内网穿透工具通过代理的方式才能访问内网的服务,这里笔者为了节省时间选择了贝锐花生壳这款内网穿透产品。

首先需要下载花生壳8客户端,下载地址:https://hsk.oray.com/download/?utm_source=baidu&utm_medium=cpc&utm_campaign=hsk_download&referral_id=47020&wordid=pc010090075202307250000101&adp=cl2&bd_vid=7814475232597963386

这里我下载的是Windows系统8.13版本的花生壳客户端,其他如Mac和Linux系统的用户可下载与之对应的花生壳客户端软件。

然后参考快速入门教程完成账号的登录和注册(注册后需要实名认证,只需要上传身份证正反面即可),注册成功后需要记住自己的账户(这个账户是一个中等长度的字符串)和密码,后面再个人电脑上的客户端登录的时候是需要用到的,可以复制下来保存到特定文件夹下的txt格式文件中。

注册成功后系统会自动给我们分配一个体验版的壳域名,例如我的壳域名是:ss8971sw6958.vicp.fun 这个域名是可以通过外网访问的,这个过程需要花上6-9块钱,在添加客户端映射的时候只是开通http或https其中一个协议,只需要6块钱,但是需要同时开通httphttps协议则需要9块钱。

添加内网映射之前需要在个人联网电脑的PowerShell命令控制台中输入ipconfig命令回车后查看内网电脑的IP地址

IpAddress.png

图 5 通过 ipconfig 命令查看本地机器ip地址

映射类型选择HTTPS, 然后将通过ipconfig命令查出来的个人联网机器IPv4对应的地址,复制到新增映射信息页面中的内网主机下面的输入框中,内网端口输入80,然后点击确定按钮完成添加映射

mapping_config.png

图 6 花生壳内网穿透添加映射

添加完成映射后可以在贝锐花生壳管理平台的->内网穿透页面看到自己配置的映射数据,后面等我们启动了本地服务并登上花生壳客户端后还需要点击右边的诊断按钮确认我们配置的内网穿透是否可用。

client_mapping_domain.png

图 7 体验版内网穿透

注意:如果选择了https协议,无需在内网服务项目中生成导入ssl安全证书,ssl安全证书的事情花生壳平台帮我们做了。如果我们继续在自己的内网服务项目中导入ssl安全证书的话反而会报 502 Bad Gateway 错误,笔者当时就踩了这个坑。若是不小心在内网项目中导入了ssl安全证书,且内网加入映射配置后诊断也通过了,那就需要删除内网服务项目中的ssl安全证书后并删除原来的映射再重新添加就好了。

如果不愿意花钱的读者朋友也可以部署开源中微子代理服务实现内网穿透,这种方式实现内网穿透需要用户有自己的云服务器,如何部署请参考链接 https://gitee.com/dromara/neutrino-proxy/blob/master/docs/快速使用.MD

引入阿里支付 SDK 依赖坐标

1)首先我们需要在项目的pom.xml文件中添加支付宝的Java SDK依赖;

<!-- 支付宝支付Java SDK -->
<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.38.72.ALL</version>
</dependency>

2)添加配置项

application.yml文件中添加支付宝支付的相关配置变量

alipay:
  gatewayUrl: https://openapi-sandbox.dl.alipaydev.com/gateway.do
  appId: 9021000135678698
  alipayPublicKey: #从沙箱应用中拷贝来的阿里支付公钥
  appPrivateKey: #从沙箱应用中拷贝来的应用私钥
  encryptKey: #从沙箱应用中接口内容加密方式查看到的加密密钥
  returnUrl: #支付成功后的跳转链接
  notifyUrl: https://ss8971sw6958.vicp.fun/mall-portal/alipay/notify #支付成功后的回调通知链接

3)创建一个支付宝支付的配置类AlipayConfig,这个配置类可以从application.yml中读取支付宝配置

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "alipay")
public class AlipayConfig {
    /**
     * 支付宝网关
     */
    private String gatewayUrl;
    /**
     * 支付宝分配给开发者的应用ID
     */
    private String appId;
    /**
     * 开发者私钥,由开发者自己生成
     */
    private String appPrivateKey;
    /**
     * 支付宝公钥,由支付宝生成。
     */
    private String alipayPublicKey;
    /**
     * 用户确认支付后,支付宝调用的页面返回路径
     */
    private String returnUrl;
    /**
     * 支付宝服务器主动通知商户服务器里的异步通知回调(需要公网能访问)
     */
    private String notifyUrl;
    /**
     * 参数返回格式,只支持JSON
     */
    private String format = "JSON";
    /**
     * 请求使用的编码格式
     */
    private String charset = "UTF-8";
    /**
     * 生成签名字符串所使用的签名算法类型
     */
    private String signType = "RSA2";
}

4)新建支付宝请求客户端配置类AlipayClientConfig,用于向支付宝API发送请求

@Configuration
public class AlipayClientConfig {

    @Bean
    public AlipayClient alipayClient(AlipayConfig config){
        return new DefaultAlipayClient(config.getGatewayUrl(),config.getAppId(),config.getAppPrivateKey(), config.getFormat(),config.getCharset(),config.getAlipayPublicKey(),config.getSignType());
    }
}

订单管理接口实现

1)首先创建alipay_order表,用于存储订单信息

CREATE TABLE `alipay_order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `order_id` varchar(64) NOT NULL COMMENT '订单ID',
  `subject` varchar(255) DEFAULT NULL COMMENT '订单标题/商品标题/交易标题',
  `total_amount` decimal(10,2) DEFAULT NULL COMMENT '订单总金额',
  `trade_status` varchar(255) DEFAULT NULL COMMENT '交易状态',
  `trade_no` varchar(255) DEFAULT NULL COMMENT '支付宝交易号',
  `buyer_id` varchar(255) DEFAULT NULL COMMENT '买家支付宝账号',
  `gmt_payment` datetime DEFAULT NULL COMMENT '交易付款时间',
  `buyer_pay_amount` decimal(10,2) DEFAULT NULL COMMENT '用户在交易中支付的金额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COMMENT='支付宝支付订单表';

2)新建一个AlipayOrder实体类用于映射alipay_order表中的数据行

package com.macro.mall.model;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Getter;
import lombok.Setter;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;

@Getter
@Setter
public class AlipayOrder implements Serializable {

    private Long id;
    /**
     * 订单ID
     */
    private String orderId;
    /**
     * 订单标题/商品标题/交易标题
     */
    private String subject;

    /**
     * 订单总金额
     */
    private BigDecimal totalAmount;

    /**
     * 交易状态
     */
    private String tradeStatus;

    /**
     * 支付宝交易号
     */
    private String tradeNo;
    /**
     * 买家支付宝账号
     */
    private String buyerId;
    /**
     * 交易付款时间,固定日期格式,使用东八区时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date gmtPayment;

    /**
     * 用户在交易中支付的金额
     */
    private BigDecimal buyerPayAmount;

}

3)创建支付宝订单管理控制器AlipayOrderController

package com.macro.mall.portal.controller;


import com.macro.mall.common.api.CommonResult;
import com.macro.mall.model.AlipayOrder;
import com.macro.mall.portal.service.AlipayOrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@RequestMapping("/alipayOrder")
@Api(tags = "AlipayOrderController")
@Tag(name = "AlipayOrderController", description = "支付宝订单管理")
public class AlipayOrderController {

     @Resource
    private  AlipayOrderService alipayOrderService;

    @ApiOperation("创建订单")
    @PostMapping("/createOrder")
    public CommonResult<AlipayOrder> createOrder(){
        AlipayOrder order = alipayOrderService.createOrder();
        return CommonResult.success(order);
    }

    @ApiOperation("查询订单")
    @GetMapping("/order")
    public CommonResult<AlipayOrder> getOrderInfo(@RequestParam("orderId") String orderId){
        AlipayOrder order = alipayOrderService.getOrderInfo(orderId);
        return CommonResult.success(order);
    }
}

4)创建支付宝订单管理AlipayOrderService接口及实现类。

package com.macro.mall.portal.service.impl;

import cn.hutool.core.util.RandomUtil;
import com.macro.mall.mapper.AlipayOrderMapper;
import com.macro.mall.model.AlipayOrder;
import com.macro.mall.portal.service.AlipayOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;

@Slf4j
@Service
public class AlipayOrderServiceImpl implements AlipayOrderService {
    @Autowired
    private AlipayOrderMapper alipayOrderMapper;

    @Override
    public AlipayOrder createOrder() {
        String orderId = new SimpleDateFormat("yyyyMMdd").format(new Date()) + System.currentTimeMillis();
        AlipayOrder alipayOrder = new AlipayOrder();
        alipayOrder.setOrderId(orderId);
        //模拟数量
        int quantity = RandomUtil.randomInt(1, 10);
        BigDecimal price = new BigDecimal(20);
        alipayOrder.setSubject("测试商品"+quantity+"个");
        BigDecimal totalAmount = price.multiply(new BigDecimal(quantity));
        alipayOrder.setTotalAmount(totalAmount);
        alipayOrder.setGmtPayment(new Date());
        alipayOrder.setBuyerPayAmount(totalAmount);
        alipayOrderMapper.addOrder(alipayOrder);
        return alipayOrder;
    }

     @Override
    public AlipayOrder getOrderInfo(String orderId) {
        log.info("orderId={}", orderId);
        return alipayOrderMapper.getOrder(orderId);
    }
}

5)创建mapper类及其xml映射文件

@Repository
public interface AlipayOrderMapper {

    void addOrder(AlipayOrder alipayOrder);

    AlipayOrder getOrder(String orderId);

    void updateTradeOrderByOrderId(AlipayOrder alipayOrder);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.macro.mall.mapper.AlipayOrderMapper">
    <resultMap id="alipayOrderMap" type="com.macro.mall.model.AlipayOrder">
        <id  column="id" property="id" jdbcType="BIGINT" javaType="java.lang.Long"/>
        <result column="order_id" property="orderId" javaType="java.lang.String" jdbcType="VARCHAR"/>
        <result column="subject" property="subject" javaType="java.lang.String" jdbcType="VARCHAR"/>
        <result column="total_amount" property="totalAmount" javaType="java.math.BigDecimal" jdbcType="DECIMAL"/>
        <result column="trade_status" property="tradeStatus" javaType="java.lang.String" jdbcType="VARCHAR"/>
        <result column="trade_no" property="tradeNo" javaType="java.lang.String" jdbcType="VARCHAR"/>
        <result column="buyer_id" property="buyerId" javaType="java.lang.String" jdbcType="VARCHAR"/>
        <result column="gmt_payment" property="gmtPayment" javaType="java.util.Date" jdbcType="TIMESTAMP"/>
        <result column="buyer_pay_amount" property="buyerPayAmount" javaType="java.math.BigDecimal" jdbcType="DECIMAL"/>
    </resultMap>

    <insert id="addOrder" parameterType="com.macro.mall.model.AlipayOrder">
        insert into alipay_order(order_id, subject, total_amount, trade_status, trade_no,
                                 buyer_id, gmt_payment,buyer_pay_amount)
        values(#{orderId,jdbcType=VARCHAR}, #{subject, jdbcType=VARCHAR}, #{totalAmount,jdbcType=DECIMAL},
               #{tradeStatus,jdbcType=VARCHAR}, #{tradeNo,jdbcType=VARCHAR}, #{buyerId,jdbcType=VARCHAR},
               #{gmtPayment,jdbcType=TIMESTAMP}, #{buyerPayAmount,jdbcType=DECIMAL})
    </insert>
    <update id="updateTradeOrderByOrderId" parameterType="com.macro.mall.model.AlipayOrder">
        update alipay_order set trade_status = #{tradeStatus,jdbcType=VARCHAR},
                            trade_no = #{tradeNo, jdbcType=VARCHAR},
                            buyer_id = #{buyerId, jdbcType=VARCHAR}
        where order_id = #{orderId,jdbcType=VARCHAR}
    </update>
    <select id="getOrder" resultType="com.macro.mall.model.AlipayOrder" parameterType="string" resultMap="alipayOrderMap">
      select id, order_id, subject, total_amount, trade_status,
             trade_no, buyer_id, gmt_payment, buyer_pay_amount
      from mall.alipay_order
      where order_id = #{orderId,jdbcType=VARCHAR}
    </select>
</mapper>

支付接口实现

1)创建与支付宝支付相关的控制器AlipayController

package com.macro.mall.portal.controller;

import com.macro.mall.common.api.CommonResult;
import com.macro.mall.model.AliPayParam;
import com.macro.mall.portal.config.AlipayConfig;
import com.macro.mall.portal.service.AlipayService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Map;
import java.util.HashMap;
// 注意这个控制器中的aliPayByType接口需要在浏览器中写入html格式的内容,因此不可用RestController注解
@Controller
@Api(tags = "AlipayController")
@Tag(name = "AlipayController", description = "支付宝支付相关接口")
@RequestMapping("/alipay")
public class AlipayController {

     @Resource
    private AlipayConfig alipayConfig;

    @Resource
    private AlipayService alipayService;

   @ApiOperation("同时支持PC端和手机移动端的支付")
    @RequestMapping(value = "/payByType", method = RequestMethod.GET)
    public void aliPayByType(@RequestParam("outTradeNo") String outTradeNo, @RequestParam("totalAmount") BigDecimal totalAmount, @RequestParam("subject") String subject,  @RequestParam("payType") Integer payType, HttpServletResponse response) throws IOException {
        response.setContentType("text/html;charset=" + alipayConfig.getCharset());
        AliPayParam aliPayParam = new AliPayParam();
        aliPayParam.setPayType(payType);
        aliPayParam.setOutTradeNo(outTradeNo);
        aliPayParam.setSubject(subject);
        aliPayParam.setTotalAmount(totalAmount);
        response.getWriter().write(alipayService.payByType(aliPayParam));
        response.getWriter().flush();
        response.getWriter().close();
    }

    @ApiOperation(value = "支付宝异步回调",notes = "必须为POST请求,执行成功返回success,执行失败返回failure")
    @RequestMapping(value = "/notify", method = RequestMethod.POST)
    public String notify(HttpServletRequest request){
        Map<String, String> params = new HashMap<>();
        Map<String, String[]> requestParams = request.getParameterMap();
        for(String name: requestParams.keySet()){
            params.put(name, request.getParameter(name));
        }
        return alipayService.notify(params);
    }

    @ApiOperation(value = "支付宝统一收单线下交易查询",notes = "订单支付成功返回:TRADE_SUCCESS")
    @RequestMapping(value = "/queryOrder", method = RequestMethod.GET)
    @ResponseBody
    public CommonResult<String> queryOrder(@RequestParam("outTradeNo") String outTradeNo, @RequestParam(value = "tradeNo", required = false) String tradeNo){
        return CommonResult.success(alipayService.query(outTradeNo,tradeNo));
    }
}

2)AlipayParam 类的定义如下:

@Getter
@Setter
public class AliPayParam implements Serializable {
    /**
     * 商户订单号
     */

    private String outTradeNo;
    /**
     * 支付总金额
     */
    private BigDecimal totalAmount;
    /**
     * 商品标题和描述
     */
    private String subject;

    /**
     * 支付类型:1-PC端支付; 2-手机支付
     */
    private Integer payType;
}

3) 创建支付宝支付Service及实现类

@Slf4j
@Service
public class AlipayServiceImpl implements AlipayService {
    @Resource
    private AlipayConfig alipayConfig;
    @Resource
    private AlipayClient alipayClient;
    @Resource
    private AlipayOrderMapper alipayOrderMapper;

    /**
     * PC端支付宝支付
     * @param aliPayParam 支付参数
     * @return formHtml
     */
    @Override
    public String payByType(AliPayParam aliPayParam) {
        log.info("aliPayParam={}", JSONUtil.toJsonStr(aliPayParam));
        AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
        if(StringUtils.isNotEmpty(alipayConfig.getNotifyUrl())){
            //异步接收地址,公网可访问
            request.setNotifyUrl(alipayConfig.getNotifyUrl());
        }
        if(StringUtils.isNotEmpty(alipayConfig.getReturnUrl())){
            request.setReturnUrl(alipayConfig.getReturnUrl());
        }
        //******必传参数******
        JSONObject bizContent = new JSONObject();
        //商户订单号,商家自定义,保持唯一性
        bizContent.set("out_trade_no", aliPayParam.getOutTradeNo());
        //支付金额,最小值0.01元
        bizContent.set("total_amount", aliPayParam.getTotalAmount());
        //订单标题,不可使用特殊符号
        bizContent.set("subject", aliPayParam.getSubject());
        if(aliPayParam.getPayType() ==1){
            //电脑网站支付场景固定传值FAST_INSTANT_TRADE_PAY
            bizContent.set("product_code", "FAST_INSTANT_TRADE_PAY");
        } else {
            //手机网站支付默认传值QUICK_WAP_WAY
            bizContent.set("product_code", "QUICK_WAP_WAY");
        }
        request.setNeedEncrypt(true);
        request.setBizContent(bizContent.toString());
        String formHtml = null;
        try {
            formHtml = alipayClient.pageExecute(request).getBody();
        } catch (AlipayApiException e) {
            log.error("AlipayClient#pageExecute failed", e);
        }
        return formHtml;
    }

    @Override
    public String notify(Map<String, String> params) {
        log.info("notify_params={}", JSONUtil.toJsonStr(params));
        String result = "failure";
        boolean signVerified = false;
        //调用SDK验证签名
        try {
            signVerified = AlipaySignature.rsaCheckV1(params, alipayConfig.getAlipayPublicKey(), alipayConfig.getCharset(), alipayConfig.getSignType());
        } catch (AlipayApiException e) {
            log.error("支付回调签名校验异常!",e);
        }
        if(signVerified){
            String tradeStatus = params.get("trade_status");
            if("TRADE_SUCCESS".equals(tradeStatus)){
                result = "success";
                AlipayOrder alipayOrder = new AlipayOrder();
                alipayOrder.setOrderId(params.get("out_trade_no"));
                alipayOrder.setTradeStatus(tradeStatus);
                alipayOrder.setBuyerId(params.get("buyer_id"));
                alipayOrder.setTradeNo(params.get("trade_no"));
                log.info("notify方法被调用了,alipayOrder:{}",JSONUtil.toJsonStr(alipayOrder));
                //根据orderId查询订单,并修改订单状态
                alipayOrderMapper.updateTradeOrderByOrderId(alipayOrder);

            } else {
                log.warn("订单未支付成功,trade_status:{}", tradeStatus);
            }
        }
        return result;
    }

    @Override
    public String query(String outTradeNo, String tradeNo) {
        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
        //******必传参数******
        JSONObject bizContent = new JSONObject();
        //设置查询参数,out_trade_no和trade_no至少传一个
        if(StringUtils.isNotEmpty(outTradeNo)){
            bizContent.set("out_trade_no", outTradeNo);
        }
        if(StringUtils.isNotEmpty(tradeNo)){
            bizContent.set("trade_no", tradeNo);
        }
        //交易结算信息: trade_settle_info
        String[] queryOptions = {"trade_settle_info"};
        bizContent.set("queryOptions", queryOptions);
        request.setBizContent(bizContent.toString());
        AlipayTradeQueryResponse response = null;
        try {
            response = alipayClient.execute(request);
            log.info("response={}", JSONUtil.toJsonStr(response));
        } catch (AlipayApiException e) {
            log.error("查询支付宝账单异常!",e);
        }
        if(response!=null){
            log.info("查询支付宝账单成功!");
            //交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)、TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)、TRADE_SUCCESS(交易支付成功)、TRADE_FINISHED(交易结束,不可退款)
            if(response.isSuccess()){
                return response.getTradeStatus();
            } else {
                return  response.getSubCode();
            }
        } else {
            log.error("查询支付宝账单失败!");
            return "TRADE_ERROR";
        }

    }
}

改mall-gateway 微服务项目

因为配置的内网穿透映射的内网服务端口为80,所以需要将mall-gateway项目中application.yml文件中的server.port值改为80,同时通过防火墙开放80端口服务

server:
  port: 80

mall-gateway网关项目用了Spring-Security来做安全认证和鉴权,由于暂时没有写前端页面,为了方便我们直接在浏览器中通过Get请求提交支付成功后页面能弹出支付宝的支付界面,我们需要对这次在mall-portal项目中添加的与创建订单和支付相关的接口全部先免鉴权。

secure:
  ignore:
    urls: 
      - "/mall-portal/alipay/**"
      - "/mall-portal/alipayOrder/**"

功能测试

启动服务并诊断内网穿透客户端

启动mall-swarm项目的两个微服务mall-gatewaymall-portal和服务前需要子在本地先启动MysqlredisrabbitmqnaocsMongodb等服务,然后再依次启动mall-gateway` 和mall-portal `两个微服务。

启动微服务成功后在自己的电脑桌面登录花生壳客户端,开启自定义诊断,出现如下诊断信息显示为连接成功的情况表示内网穿透服务开启成功。

innerThroughConnectSuccess.png

图 8 花生壳客户端应用内网穿透连接测试

支付效果测试

1)在ApiPost中调用创建订单接口,获取订单号和 支付金额等数据

POST https://ss8971sw6958.vicp.fun/mall-portal/alipayOrder/createOrder
// 接口返回数据
{
	"code": 200,
	"message": "操作成功",
	"data": {
		"orderId": "202404091712673177214",
		"subject": "测试商品2个",
		"totalAmount": 40,
		"gmtPayment": "2024-04-09 22:32:57",
		"buyerPayAmount": 40
	}
}

未提交支付前根据order_id='202404091712673177214' 从alipay_order表中查询出来的订单数据订单状态order_status字段为空

trade_status_null.png

图 8 未提交支付前本地数据库订单表订单数据

2)将创建订单接口的返回参数作为支付接口的入参在谷歌浏览器中调用支付接口

submitAliPay.png

图 9 浏览器中输入提交订单支付链接

回车后返回支付宝支付界面

aliPayPage.png

图 10 支付宝弹出的订单支付信息界面

3)然后进入沙箱支付测试账号买家信息页面查看完整的账户名和支付密码后输入后点击【下一步】进入支付确认页面。

confirmPayPage.png

图 11 支付宝订单支付确认界面

4)点击确认付款后页面弹出付款成功信息

pay_success.png

图 12 支付宝付款成功消息提示界面

5)支付成功后我们可以在mall-portal服务的控制台中根据关键字 notify_params 查到对应的支付宝对mall-portal服务的支付结果回调参数

2024-04-09 22:43:25 [http-nio-8085-exec-6] INFO  c.m.m.p.s.impl.AlipayServiceImpl
 - notify_params={"gmt_create":"2024-04-09 22:42:44","charset":"UTF-8","gmt_payment":"2024-04-09 22:43:22","notify_time":"2024-04-09 22:43:24","subject":"测试商品2个","sign":"PSVSnQL6O8veDDBBlBctgAiBUWY8pa3l9RDcWRRCeer6Nkvos/4F2hiIm49X2s5H3RIbhl+lWquG7CmYurCKfBKvVgallWakJGBooY52sYDYWE95IBkFo6x59aZ+erF3IpX8EsFXKFiBuuJJytT91Wy2IAIAcTRaYnIdMaOTd6AxqJ1Pabc0MSSzdUeSRFIXnd3dbLOX8lZyANLZOjE6sqiLYS9Xp2Co6UvDJNJltxhakc8fEQE3VRi8CTrzce99qxM8kLb0iyyMgcd4EykDwxR65Yn5K7mmNticbh9D0FqhWPCdH8Dvt2yAwY3GjQlGvsz/EJ3JcjFnQWd2f3mwow==","buyer_id":"2088722032870579","invoice_amount":"40.00","version":"1.0","notify_id":"2024040901222224323070570502795855","fund_bill_list":"[{\"amount\":\"40.00\",\"fundChannel\":\"ALIPAYACCOUNT\"}]","notify_type":"trade_status_sync","out_trade_no":"202404091712673177214","total_amount":"40.00","trade_status":"TRADE_SUCCESS","trade_no":"2024040922001470570502624165","auth_app_id":"9021000135678698","receipt_amount":"40.00","point_amount":"0.00","buyer_pay_amount":"40.00","app_id":"9021000135678698","sign_type":"RSA2","seller_id":"2088721032870561"}

6)再次根据orderId的值执行查询支付订单的sql脚本可以看到 trade_status、trade_no 和 buyer_id 等三个之前为null 的字段都已更新

update_trade_status.png

图 13 提单提交支付成功后本地数据库订单表数据变化

trade_status字段值为 TRADE_SUCCESS 表示交易成功。

7)查询支付结果接口

如果没有开通内网穿透服务,那么我们就只能通过调用查询支付结果查询订单的交易状态

在ApiPost中调用以下GET接口

GET https://ss8971sw6958.vicp.fun/mall-portal/alipay/queryOrder?outTradeNo=202404091712673177214
// 返回响应信息
{
	"code": 200,
	"message": "操作成功",
	"data": "TRADE_SUCCESS"
}

响应数据中的data字段值就是订单支付后的交易状态值。

总结

本文通过集成支付宝支付,在mall微服务项目中实现了一套支付流程,使用内网穿透接收到订单支付后来自阿里支付结果的回调数据。这里也仅仅只是实现了在线支付,支付里面的场景还是比较多的,包括扫码支付、当面付和申请退款等场景,只不过只是接口和参数不同而已。

其实支付功能并不难实现,支付宝沙箱环境让我们无需复杂的商业流程,就可以在沙箱环境快速实现支付功能。但是打通线上正式支付的场景时还是需要我们在支付宝开发平台创建对应的网站应用或小程序并开通产品,然后提交营业执照等材料申请支付宝签约。而如果没有营业执照的前提下仍然想要开通个人应用的支付功能,可以YunGouOS平台开通服务商模式签约实现应用的支付功能。

本文首发个人微信公众号【阿福谈Web编程】,有不懂的地方可以通过关注我的微信公众号,然后点击【联系作者】菜单可以获得笔者的个人微信联系方式,大家一起交流进步。