Java支付宝扫码支付[新]

支付宝老版本的 Web 端扫码支付产品为即时到账,更新到新版后现在的产品为电脑网站支付

官网

电脑网站支付快速接入

准备数据

  1. 电脑网站支付 应用需要在开发者页面端进行申请和签约
  2. 应用签约成功之后需要获取以下信息
    • app_id: 签约成功的应用唯一标识
    • app_private_key: 通过 RSA 密钥生成的应用私钥,由开发者自己通过签名软件生成[支付宝提供]
    • alipay_public_key: 通过应用私钥和公钥生成的支付宝公钥,由支付宝自动生成

下载 Java 版 SDK

  1. 前往 电脑网站支付 SDK 获取 下载 Java 的 SDK
  2. 该 SDK 目前不支持 Maven 方式引入,只能通过本地引入
  3. 正常开发模式只需要引入 alipay-sdk-java-3.0.0.jar 即可

获取支付宝客户端

  1. 支付宝客户端是调用所有接口的前置条件,所以应该放置在顶层父类做一次初始化
  2. SOPConstants.ALI_PAY_SEND_URL 是该支付方式的统一请求接口,值为 https://openapi.alipay.com/gateway.do
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class AliCoreServiceImpl extends OrderCoreServiceImpl {

    // 参数返回格式
    private static final String ALI_PAY_FORMAT = "json";

    // 编码集,支持 GBK/UTF-8
    protected static final String ALI_PAY_CHARSET = "utf-8";

    // 商户生成签名字符串所使用的签名算法类型,目前支持 RSA2/RSA ,推荐使用 RSA2
    protected static final String ALI_PAY_SIGN_TYPE = "RSA2";

    // 订单支付成功状态
    protected static final String TRADE_STATUS_SUCCESS = "TRADE_SUCCESS";

    // 订单支付结束状态
    protected static final String TRADE_STATUS_FINISHED = "TRADE_FINISHED";

    // 支付宝客户端
    protected static AlipayClient alipayClient;

    static {
        // 初始化支付宝客户端
        alipayClient = new DefaultAlipayClient(
                SOPConstants.ALI_PAY_SEND_URL,
                SOPConstants.ALI_PAY_APP_ID,
                SOPConstants.ALI_PAY_PRIVATE_KEY,
                ALI_PAY_FORMAT,
                ALI_PAY_CHARSET,
                SOPConstants.ALI_PAY_PUBLIC_KEY,
                ALI_PAY_SIGN_TYPE);
    }
}

发起支付宝付款页面请求

  1. 新版的扫码支付对请求参数响应参数都做了封装,只需要传入对应参数值即可
  2. SOPConstants.ALI_PAY_RETURN_URL 是支付宝付款成功需要的同步回执地址
  3. SOPConstants.ALI_PAY_NOTIFY_URL 是支付宝付款成功需要的异步回执地址
  4. 以上两个地址都需要能够外网访问
  5. 该请求方式是通过拼接请求使用 iframe 获取支付宝的付款二维码
    • 通过客户端发起请求时不使用一般的 pageExecute() 而使用 sdkExecute()
    • sdkExecute() 获取的是请求之后的参数值,所以还需要拼接请求地址
    • SOPConstants.ALI_PAY_SEND_URL 是支付请求地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class AliPayServiceImpl extends AliCoreServiceImpl {

    public String aliPay(Long orderId) {
        // 创建统一下单请求
        AlipayTradePagePayRequest payRequest = new AlipayTradePagePayRequest();
        payRequest.setReturnUrl(SOPConstants.ALI_PAY_RETURN_URL);
        payRequest.setNotifyUrl(SOPConstants.ALI_PAY_NOTIFY_URL);

        // 将订单详情传入业务请求参数中
        payRequest.setBizModel(generateOrderInfo(orderId));

        String body = null;
        try {
            body = alipayClient.sdkExecute(payRequest).getBody();
        } catch (AlipayApiException e) {
            logger.error("订单" + orderId + "获取支付宝付款界面失败!");
        }

        if (body == null) {
            logger.error("订单" + orderId + "未成功获取支付宝付款界面!");
        }

        return SOPConstants.ALI_PAY_SEND_URL + "?" + body;
    }

    private AlipayTradePagePayModel generateOrderInfo(Long orderId) {
        // 获取订单信息
        OrderDTO order = orderManageService.getOrder(orderId);

        AlipayTradePagePayModel model = new AlipayTradePagePayModel();
        model.setOutTradeNo(order.getCode());
        model.setProductCode("FAST_INSTANT_TRADE_PAY");
        model.setTotalAmount(String.valueOf(order.getPrice()));
        model.setSubject("实训在线-" + order.getName());
        model.setBody("实训在线-" + order.getName());
        // 二维码获取方式为模式4,最简洁模式,只有二维码
        model.setQrPayMode("4");
        model.setQrcodeWidth(240L);

        return model;
    }

}
  1. 控制层调用请求接口获取完整请求地址
1
2
3
4
5
6
7
8
@RequestMapping("/ali/{orderId}")
public String ali(Model model, @PathVariable String orderId) {
    model.addAttribute("order", orderManageService.getOrder(IdEncoder.decodeId(orderId)));
    model.addAttribute("orderType", OrderPay.aliPay.getCode());
    model.addAttribute("aliPayUrl", aliPayService.aliPay(IdEncoder.decodeId(orderId)));

    return "/order/pay/ali";
}
  1. 上述方法中拼接完整的二维码请求地址可以直接放置到 iframe 的 src 属性中
1
<iframe id="aliPayFrame" src="${aliPayUrl}" frameborder="0"></iframe>

接收同步回执

  1. 该地址需要与之前初始化客户端时提交给支付宝的同步回执地址保持一致
  2. 回执地址不能有任何多余参数
1
2
3
4
5
6
@RequestMapping("/ali/return")
public String aliReturn(HttpServletRequest request) {
    aliNotifyService.aliReturn(request);

    return null;
}
  1. 新版的同步回执中不会携带订单的状态信息,所以只能用做一般的信息接收,不能做为交易成功的依据
    • 同步回执需要验签,但验签的方法支付宝已提供,即 AlipaySignature.rsaCheckV2()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void aliReturn(HttpServletRequest request) {
    // 订单号
    String orderCode = request.getParameter("out_trade_no");

    Boolean result = false;
    try {
        // 回执验签
        result = AlipaySignature.rsaCheckV2(parameterToMap(request, false), SOPConstants.ALI_PAY_PUBLIC_KEY, ALI_PAY_CHARSET, ALI_PAY_SIGN_TYPE);
    } catch (AlipayApiException e) {
        logger.error("支付宝订单" + orderCode + "同步回执验签失败!", e);
    }

    if (result) {
        logger.info("支付宝订单" + orderCode + "同步回执接收成功!");
    } else {
        logger.error("支付宝订单" + orderCode + "同步回执无效!");
    }
}
  1. 对同步回执中的响应参数验签之前,需要先将参数转化为 map 形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private Map<String, String> parameterToMap(HttpServletRequest request, Boolean isNotify) {
    // 获取回执内容
    Map requestParams = request.getParameterMap();

    // 过滤回执内容
    Map<String, String> params = Maps.newHashMap();
    for (Object o : requestParams.keySet()) {
        String name = (String) o;
        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] + ",";
        }

        if (isNotify) {
            params.put(name, valueStr);
        } else {
            try {
                params.put(name, new String(valueStr.getBytes("ISO-8859-1"), "utf-8"));
            } catch (UnsupportedEncodingException e) {
                logger.error("支付宝订单同步回执转码失败!", e);
            }
        }
    }

    return params;
}

接收异步回执

  1. 该地址需要与之前初始化客户端时提交给支付宝的异步回执地址保持一致
  2. 回执地址不能携带任何参数
1
2
3
4
5
@RequestMapping(value = "/ali", method = RequestMethod.POST)
@ResponseBody
public String aliNotify(HttpServletRequest request) {
    return aliNotifyService.aliNotify(request);
}
  1. 异步回执作为支付成功的依据,支付宝成功通知到本地后,需要明确告知支付宝交易已成功
    • 交易成功后返回 success 即可
    • 当订单支付成功后订单可以展现两种状态,TRADE_SUCCESSTRADE_FINISHED
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public String aliNotify(HttpServletRequest request) {
    // 订单号
    String orderCode = request.getParameter("out_trade_no");
    // 订单状态
    String orderStatus = request.getParameter("trade_status");

    boolean result = false;
    try {
        result = AlipaySignature.rsaCheckV2(parameterToMap(request, true), SOPConstants.ALI_PAY_PUBLIC_KEY, ALI_PAY_CHARSET, ALI_PAY_SIGN_TYPE);
    } catch (AlipayApiException e) {
        logger.error("支付宝订单" + orderCode + "异步回执验签失败,订单状态为:" + orderStatus + "!", e);
    }

    // 验签成功
    if (result) {
        // 订单已支付
        if (orderStatus.equals(TRADE_STATUS_SUCCESS) || orderStatus.equals(TRADE_STATUS_FINISHED)) {
            // 更新订单状态
            updateOrderInfo(orderCode, OrderPay.aliPay.getCode());

            return "success";
        } else {
            logger.error("支付宝订单" + orderCode + "未支付,订单状态为:" + orderStatus + "!");
            return "fail";
        }
    } else {
        logger.error("支付宝订单" + orderCode + "异步回执接收失败,订单状态为:" + orderStatus + "!");
        return "fail";
    }
}