支付宝扫码支付Spring版本

支付宝扫码支付Spring版本

在阿里官方给的支付宝扫码支付demo中,使用的是jsp版本,今天参照官方demo将其整合到Spring中。

我们主要做个demo模拟Q币充值过程,其主要功能:

1、登录,充值前得有用户登入

2、充Q币(包含:下单+支付+充值,3个过程)

如何开通支付宝沙箱或查看其它API参见《蚂蚁金服开放平台》或者自行百度。

首先介绍一下支付宝支付流程,大致如下:

在这里插入图片描述

实际上的流程远没有那么简单,支付流程伴随的各种数据会存储在支付宝服务器中。

现支付宝的通知有两类。

​ A、异步通知,也就是服务器通知,对应的参数为notify_url,支付宝通知使用POST方式 ,告诉服务器该订单已支付成功
在这里插入图片描述

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

​ B、同步通知,也就是页面跳转通知,对应的参数为return_url,支付宝通知使用GET方式 ,主要是给用户看以便后续操作

支付成功后支付宝会向并且异步通知的处理是非常重要的,由于项目部署在本地,支付宝无法获取success反馈(公网无法访问到本地服务器),我们仅仅做同步通知处理。

==================================== 项目开始 ====================================

​ 技术:SpringBoot + Freemarker

项目结构:

在这里插入图片描述

​ application.properties配置

###FREEMARKER (FreeMarkerAutoConfiguration)###
# 设定ftl文件路径
spring.freemarker.template-loader-path=classpath:/templates
spring.freemarker.cache=true
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.request-context-attribute=request
spring.freemarker.suffix=.ftl

0、导入alipay官方lib

这里我已经提前将lib导入本地Maven仓库了,不会的小伙伴可以自行百度“添加jar包到本地Maven仓库”,或者手动导包,lib在阿里官方支付宝demo附带。
在这里插入图片描述

  <!--  支付宝依赖 -->
        <dependency>
            <groupId>com.alipay</groupId>
            <artifactId>alipay-sdk</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.alipay</groupId>
            <artifactId>alipay-sdk-source</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.alipay</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.1</version>
        </dependency>

1、4个前端页面:

登录页,首页(充值页),充Q币成功页,充Q币失败页

​ login.ftl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <h1>登录</h1>
    <form action="/index">
        <input type="text" name="username" placeholder="用户名"/>
        <input  type="password" name="password"placeholder="密码"/>
        <input  type="submit" value="登录"/>
    </form>
</body>
</html>

index.ftl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <h1>首页</h1>
    <h3>充值Q币</h3>
    <form action="/recharge/alipay">
        <input  type="text" name="money"placeholder="金额"/>
        <input  type="submit" value="确定"/>
    </form>
</body>
</html>

success.ftl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>充值成功</title>
</head>
<body>
    <h1>充值成功</h1>
    <h2>用户名称:${username}</h2>
    <h2>用户账户:${accountId}</h2>
    <h2>金额:${money}</h2>
</body>
</html>

fal.ftl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>充值失败</title>
</head>
<body>
    <h1>充值失败</h1>
    <h2>${msg}</h2>
</body>
</html>

2、登录功能

​ a、登录控制层

package com.alipay.alipayspringdemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpSession;

/**
 * @author WindShadow
 * @verion 2020/1/11.
 */

@Controller
public class LoginHandler {

 // 模拟登陆验证
    public boolean loginService(String username, String password){

        return !( username == null || "".equals(username) || password == null || "".equals(password) );
    }

    @RequestMapping("/login")
    public String loginPage(){

        return "login";
    }

    @RequestMapping("/index")
    public String login(String username, String password, HttpSession session){

        if (!loginService(username,password))	return "redirect:login";
        session.setAttribute("username",username);
        return "index";
    }

}

​ b、拦截器配置

​ LoginInterceptor登录拦截器主要职责是根据session域中是否包含用户信息而决定放行。

package com.alipay.alipayspringdemo.interceptor;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * @author WindShadow
 * @verion 2019/12/28.
 */

@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 添加拦截路径
        registry.addInterceptor(new LoginInterceptor()).
                addPathPatterns("/*").
                excludePathPatterns("/login").
                excludePathPatterns("/index").
                excludePathPatterns("/recharge/notify");// 这里放行支付宝异步通知,原因是请求不来自用户,把通知拦截了就不对了

        super.addInterceptors(registry);
    }

    // 静态资源不过滤
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {

        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
        super.addResourceHandlers(registry);
    }
}

3、阿里支付模块提取

在这里插入图片描述

AlipayConfig

package com.alipay.alipayspringdemo.alipayService;
/**
 * @author WindShadow
 * @verion 2019/12/27.
 */
/*
 *类名:AlipayConfig
 *功能:基础配置类
 *详细:设置帐户有关信息及返回路径
 *说明:
 *以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己网站的需要,按照技术文档编写,并非一定要使用该代码。
 *该代码仅供学习和研究支付宝接口使用,只是提供一个参考。
 */

public class AlipayConfig {
	
//↓↓↓↓↓↓↓↓↓↓请在这里配置您的基本信息↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

	// 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
	public String appId = "9999999999";
    
	// 商户私钥,您的PKCS8格式RSA2私钥
    public String merchantPrivateKey = "这里是马赛克";
	
	// 支付宝公钥
    public String alipayPublicKey = "这里是马赛克";

	// 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
	public String notifyUrl = "http://localhost:8080/";

	// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
	public String returnUrl = "http://localhost:8080/";

	// 签名方式
	public String signType = "RSA2";
	
	// 字符编码格式
	public String charset = "utf-8";
	
	// 支付宝网关
	public String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";
	
	// 日志路径
	public String logPath = "C:\\";


//↑↑↑↑↑↑↑↑↑↑请在这里配置您的基本信息↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

    /** 
     * 写日志,方便测试(看网站需求,也可以改成把记录存入数据库)
     * @param sWord 要写入日志里的文本内容
     */
    public void logResult(String sWord) {

//        System.out.println(sWord);
//        FileWriter writer = null;
//        try {
//            writer = new FileWriter(logPath + "alipay_log_" + System.currentTimeMillis()+".txt");
//            writer.write(sWord);
//        } catch (Exception e) {
//            e.printStackTrace();
//        } finally {
//            if (writer != null) {
//                try {
//                    writer.close();
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            }
//        }
    }

	public AlipayConfig() {}

	public AlipayConfig(String app_id, String merchant_private_key, String alipay_public_key, String notify_url, String return_url, String sign_type, String charset, String gatewayUrl, String log_path) {
		this.appId = app_id;
		this.merchantPrivateKey = merchant_private_key;
		this.alipayPublicKey = alipay_public_key;
		this.notifyUrl = notify_url;
		this.returnUrl = return_url;
		this.signType = sign_type;
		this.charset = charset;
		this.gatewayUrl = gatewayUrl;
		this.logPath = log_path;
	}

	/**-------------------------------Get Set 略------------------------------**/
	


说明:

​ 这个配置类预设的值是我的支付宝沙箱信息;

​ 因为考虑到同一个系统中可能有多个收款方,如电商平台,这些配置信息都不是static的,方便生成多个不同的实例对象;

​ 然后同步通知地址(returnUrl)和异步通知地址(notifyUrl)还没有确定,原因是,假如收款方确定了,那么AlipayConfig的公钥私钥等也确定了,但是付款方充值的业务不只一种,那么通知也就不只一种,所以随时可以通过set方法更改;

TradeInfo

package com.alipay.alipayspringdemo.alipayService;
import java.io.UnsupportedEncodingException;

/**
 * @author WindShadow
 * @verion 2019/12/28.
 */

public class TradeInfo {

    //商户订单号,商户网站订单系统中唯一订单号,必填
    private String outTradeNo = null;
    //付款金额,必填
    private String totalAmount = null;
    //订单名称,必填
    private String subject = null;
    //商品描述,可空
    private String body = null;
    //超时时间,可空
    private String timeoutExpress;

    // 乱码处理
    private String deal(String str){

        try {
            return new String(str.getBytes("ISO-8859-1"),"UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return str;
    }

    // 返回交易上下文
    public String creatBizContent(){

        /**
         * 按官方的demo使用下划线的参数名(推测负责支付服务的后台不是用JAVA写的),
         * 不知都修改了会不会影响二维码页面渲染或者后台接收数据啥的,暂时不修改
         */
		//请求参数可查阅【电脑网站支付的API文档-payByUserId.trade.page.pay-请求参数】章节
        String str = "{\"out_trade_no\":\""+ outTradeNo +"\","
                + "\"total_amount\":\""+ totalAmount +"\","
                + "\"subject\":\""+ subject +"\","
                + "\"body\":\""+ body +"\","
                + "\"timeout_express\":\""+timeoutExpress+"\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}";
        return str;
    }
    /**-------------------------------Get Set 略------------------------------**/
}

说明:

​ TradeInfo类保存了一次订单的信息,其中creatBizContent()方法返回交易上下文,利于订单提交到支付宝后台生成并返回二维码页面,也就是说这个类封装了一些请求参数。在官方文档中,请求参数有很多,上面只列出了一些需要的,大家可以根据自己的需要重写TradeInfo类,记得重写creatBizContent()方法。

AlipayService

public interface AlipayService {

    /**
     *  支付宝服务接口
     */

    // 充值
    boolean pay(AlipayConfig alipayConfig, TradeInfo orderInfo, HttpServletRequest request, HttpServletResponse response);
    // 支付宝验签
    boolean rsaCheck(AlipayConfig alipayConfig, HttpServletRequest request) throws UnsupportedEncodingException;
}

说明:

​ 阿里支付接口,

​ a、根据配置对象和订单对象生成返回二维码页面给用户,遇到异常则返回false;

​ b、验签;

AlipayServiceImp

package com.alipay.alipayspringdemo.alipayService;

import com.alipay.api.*;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.*;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * @author WindShadow
 * @verion 2019/12/27.
 */

@Service
public class AlipayServiceImp implements AlipayService {

    @Override
    public boolean pay(AlipayConfig alipayConfig, TradeInfo tradeInfo, HttpServletRequest request, HttpServletResponse response) {

        // 获得初始化的AlipayClient
        AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig.gatewayUrl, alipayConfig.appId, alipayConfig.merchantPrivateKey, "json", alipayConfig.charset, alipayConfig.alipayPublicKey, alipayConfig.signType);

        // 设置请求参数
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(alipayConfig.returnUrl);
        alipayRequest.setNotifyUrl(alipayConfig.notifyUrl);
        alipayRequest.setBizContent(tradeInfo.creatBizContent());

        // 请求并加载二维码
        try {

            String result = alipayClient.pageExecute(alipayRequest).getBody();
            PrintWriter out = response.getWriter();
            out.write(result);
            out.flush();
            out.close();
        } catch (AlipayApiException e) {
            e.printStackTrace();
            ///alipayConfig.logResult(e.getErrMsg());
            System.out.println("跳转支付失败!");
            return false;
        } catch (IOException e) {
            e.printStackTrace();
            ///alipayConfig.logResult(e.getErrMsg());
            System.out.println("输出页面失败!");
            return false;
        }
        return true;
    }

    // 支付宝验签
    @Override
    public boolean rsaCheck(AlipayConfig alipayConfig, HttpServletRequest request) throws UnsupportedEncodingException {

        //获取支付宝提交过来反馈信息,同步通知为GET异步为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 = false;
        try {
            //调用SDK验证签名
            signVerified = AlipaySignature.rsaCheckV1(params, alipayConfig.alipayPublicKey, alipayConfig.charset, alipayConfig.signType);
        } catch (AlipayApiException e) {
            e.printStackTrace();
        }
        return signVerified;
    }
}

4、继续完善充Q币系统

​ 支付宝支付模块提取完成后,该轮到充值Q币模块了,大家千万不要把支付和充值混为一谈,充值的前提是已经支付完成。

充值服务:为指定账户充值

public interface RechargeService {

    void rechargeByUserId(String accountId, Double money, AlipayConfig alipayConfig, HttpServletRequest request, HttpServletResponse response);
}
package com.alipay.alipayspringdemo.service;

import com.alipay.alipayspringdemo.alipayService.AlipayConfig;
import com.alipay.alipayspringdemo.alipayService.AlipayService;
import com.alipay.alipayspringdemo.alipayService.TradeInfo;
import com.alipay.alipayspringdemo.util.TradeNoMaker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author WindShadow
 * @verion 2020/1/11.
 */

@Service
public class RechargeServiceImp implements RechargeService {

    // 订单号生成器
    @Autowired
    TradeNoMaker maker;
    // 阿里支付服务
    @Autowired
    AlipayService alipayService;
    @Override
    public void rechargeByUserId(String accountId, Double money, AlipayConfig alipayConfig, HttpServletRequest request, HttpServletResponse response) {

        // 配置订单信息
        TradeInfo tradeInfo = new TradeInfo();
        tradeInfo.setOutTradeNo(maker.ctreateTradeNo());// 由订单编号生成器生成订单号
        tradeInfo.setTotalAmount(String.valueOf(money));
        tradeInfo.setSubject("用户["+ accountId +"]进行充值");
        tradeInfo.setBody("校园卡充值");
        tradeInfo.setTimeoutExpress("1m");
        // 调用服务充值
        alipayService.pay(alipayConfig,tradeInfo,request,response);
    }
}

订单号生成器:保证每个订单有唯一编号,不然提交到支付宝后台时会出错。

public interface TradeNoMaker {

    String ctreateTradeNo();

    String ctreateTradeNo(Object... seed);
}
package com.alipay.alipayspringdemo.util;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
 * @author WindShadow
 * @verion 2019/12/28.
 */
@Service
public class TradeNoMakerImp implements TradeNoMaker {

    @Override
    public String ctreateTradeNo() {
		//根据时间戳生成
        LocalDateTime dateTime = LocalDateTime.now();
        String str = ""+dateTime.getYear()+dateTime.getMonthValue()+dateTime.getDayOfMonth()+dateTime.getHour()+dateTime.getMinute()+dateTime.getSecond();
        return str;
    }

    @Override
    public String ctreateTradeNo(Object... seed) {
        //根据种子生成
        /* 自行安排 */
        return null;
    }
}

充值控制层

package com.alipay.alipayspringdemo.controller;

import com.alipay.alipayspringdemo.alipayService.AlipayConfig;
import com.alipay.alipayspringdemo.alipayService.AlipayService;
import com.alipay.alipayspringdemo.service.RechargeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.Objects;

/**
 * @author WindShadow
 * @verion 2020/1/6.
 */

@Controller
@RequestMapping("/recharge")
public class RechargeHandler {

    private static String RETURN_URL = "http://localhost:8080/recharge/return";
    private static String NOTIFY_URL = "http://localhost:8080/recharge/notify";

    private static AlipayConfig alipayConfig = new AlipayConfig();//也可通过IOC注入
    // 支付配置通知路径
    static {

        alipayConfig.setReturnUrl(RETURN_URL);
        alipayConfig.setNotifyUrl(NOTIFY_URL);
    }

    @Autowired
    RechargeService rechargeService;
    @Autowired
    AlipayService alipayService;

    @RequestMapping("/alipay")
    public void recharge(Double money,HttpSession session, HttpServletRequest request, HttpServletResponse response){

        String username = (String)session.getAttribute("username");
        String accountId = daoGetAccountId(username);
        rechargeService.rechargeByUserId(accountId,money,alipayConfig,request,response);
    }

    // 同步通知
    @RequestMapping("/return")
    public String dealReturnUrl(HttpSession session, HttpServletRequest request,Map<String,Object> map) throws UnsupportedEncodingException {

        System.out.println("同步通知=================");
        // 验签
        boolean signVerified = alipayService.rsaCheck(alipayConfig,request);

        //↓↓↓↓↓执行业务↓↓↓↓↓//
        String username = (String)session.getAttribute("username");
        if (signVerified){

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

            // 获取用户账户
            String accontId = daoGetAccountId(username);

            // 模拟充Q币
            System.out.println("---==↓↓↓↓↓==---");
            System.out.println("|为 ["+username+" ]充值:"+money+"Q币");
            System.out.println("---==↑↑↑↑↑==---");
            map.put("username",username);
            map.put("accountId",accontId);
            map.put("money",money);

            return "success";
        }else {

            System.out.println("验签失败!");
            map.put("msg","验签失败!");
            return "fail";
        }
        //↑↑↑↑↑执行业务↑↑↑↑↑//
    }

    // 异步通知
    @RequestMapping("/notify")
    @ResponseBody
    public String dealNotifyUrl(HttpSession session, HttpServletRequest request,Map<String,Object> map) throws UnsupportedEncodingException{

        System.out.println("异步通知================");
        // 验签
        boolean signVerified = alipayService.rsaCheck(alipayConfig,request);

        //↓↓↓↓↓执行业务↓↓↓↓↓//
        /* 实际验证过程建议商户务必添加以下校验:
        1、需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,
        2、判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),
        3、校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email)
        4、验证app_id是否为该商户本身。
        */
        if(signVerified) {//验证成功

            //商户订单号
            String outTradeNo = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8");
            //支付宝交易号
            String tradeNo = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8");
            //交易状态
            String tradeStatus = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8");

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

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

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

        }else {//验证失败

            //调试用,写文本函数记录程序运行情况是否正常
            //String sWord = AlipaySignature.getSignCheckContentV1(params);
            //AlipayConfig.logResult(sWord);
            return "fail";
        }
        //↑↑↑↑↑执行业务↑↑↑↑↑//
    }
    
    // 模拟从DAO层获取账户ID
    public String daoGetAccountId(String username){

        return String.valueOf(Objects.hashCode(username));
    }
}

说明:

​ 该控制层负责当前登录用户的充值(获取其账户ID,后进行充值),以及通知处理。专注点在于同步通知处理。

最终效果:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

样式什么的就先不要在意了,其它功能如退款、订单查询等也可参考官方demo作进一步的整合。
完整demo地址:https://github.com/WindMo/alipay-spring.git
在这里插入图片描述

发布了1 篇原创文章 · 获赞 1 · 访问量 124

猜你喜欢

转载自blog.csdn.net/WindMo356/article/details/104046119