alipay 当面付扫码支付实战开发

alipay 当面付扫码支付实战开发

参考官网地址:https://opendocs.alipay.com/open/194/105072

1、当面付介绍:

当面付包括付款码支付和扫码支付两种收款方式。适用于线下实体店支付、面对面支付、自助售货机等场景。

付款码支付:商家使用扫码枪或其他扫码机具扫描用户出示的付款码,来实现收款。

扫码支付:商家提供收款二维码,由用户通过支付宝扫码支付,来实现收款。

2、参数准备

• APPID

• 商家私钥

• 支付宝公钥

• 支付回调地址

• 网关地址

• 加密签名算法RSA2

3、JAVA 代码实战,项目基础功能准备

3.1 创建 springBoot 工程

3.2 导入依赖

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-test

test

junit

junit

4.13

org.projectlombok

lombok

true

com.alipay.sdk

alipay-sdk-java

4.22.57.ALL

cn.hutool

hutool-all

5.7.22

com.google.zxing

core

3.4.1

3.3 配置yml

# 支付宝支付参数配置

alipay:

app_id: 公司支付宝的APPID

merchant_private_key: 公司支付宝商户私钥

alipay_public_key: 公司支付宝公钥

notify_url: alipay 异步回调地址

return_url: alipay 同步回调地址(我们做二维码扫码是异步操作,没用,可以不配置)

sign_type: RSA2

charset: utf-8

gatewayUrl: https://openapi.alipay.com/gateway.do

3.4 定义配置类

import lombok.Data;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.stereotype.Component;

/**

* @author: yijun.wen

* @date: 2022/3/11 5:32 下午

* @description: alipay 配置类

*/

@Data

@Component

public class AlipayConfig {

/**

* 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号

*/

@Value("${alipay.app_id}")

public String app_id;

/**

* 商户私钥,您的PKCS8格式RSA2私钥

*/

@Value("${alipay.merchant_private_key}")

public String merchant_private_key;

/**

* 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。

*/

@Value("${alipay.alipay_public_key}")

public String alipay_public_key;

/**

* 服务器异步通知页面路径 需http://格式的完整路径,不能加参数,必须外网可以正常访问

*/

@Value("${alipay.notify_url}")

public String notify_url;

/**

* 页面跳转同步通知页面路径 需http://格式的完整路径,不能加参数,必须外网可以正常访问(我们这里没用这个)

*/

@Value("${alipay.return_url}")

public String return_url;

/**

* 签名方式

*/

@Value("${alipay.sign_type}")

public String sign_type;

/**

* 字符编码格式

*/

@Value("${alipay.charset}")

public String charset;

/**

* 支付宝网关

*/

@Value("${alipay.gatewayUrl}")

public String gatewayUrl;

}

3.5 二维码生成工具类

package com.wyj.alipay.util;

import cn.hutool.extra.qrcode.BufferedImageLuminanceSource;

import com.google.zxing.*;

import com.google.zxing.common.BitMatrix;

import com.google.zxing.common.HybridBinarizer;

import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

import javax.imageio.ImageIO;

import java.awt.*;

import java.awt.geom.RoundRectangle2D;

import java.awt.image.BufferedImage;

import java.io.File;

import java.io.OutputStream;

import java.util.Hashtable;

/**

* @author: yijun.wen

* @date: 2022/3/11 5:36 下午

* @description:

*/

public class QrCodeUtil {

private static final String CHARSET = "utf-8";

private static final String FORMAT_NAME = "JPG";

// 二维码尺寸

private static final int QRCODE_SIZE = 300;

// LOGO宽度

private static final int WIDTH = 90;

// LOGO高度

private static final int HEIGHT = 90;

private static BufferedImage createImage(String content, String imgPath, boolean needCompress) throws Exception {

Hashtable hints = new Hashtable();

hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);

hints.put(EncodeHintType.CHARACTER_SET, CHARSET);

hints.put(EncodeHintType.MARGIN, 1);

BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE,

hints);

int width = bitMatrix.getWidth();

int height = bitMatrix.getHeight();

BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

for (int x = 0; x < width; x++) {

for (int y = 0; y < height; y++) {

image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);

}

}

if (imgPath == null || "".equals(imgPath)) {

return image;

}

// 插入图片

insertImage(image, imgPath, needCompress);

return image;

}

private static void insertImage(BufferedImage source, String imgPath, boolean needCompress) throws Exception {

File file = new File(imgPath);

if (!file.exists()) {

System.err.println("" + imgPath + " 该文件不存在!");

return;

}

Image src = ImageIO.read(new File(imgPath));

int width = src.getWidth(null);

int height = src.getHeight(null);

if (needCompress) { // 压缩LOGO

if (width > WIDTH) {

width = WIDTH;

}

if (height > HEIGHT) {

height = HEIGHT;

}

Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH);

BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

Graphics g = tag.getGraphics();

// 绘制缩小后的图

g.drawImage(image, 0, 0, null);

g.dispose();

src = image;

}

// 插入LOGO

Graphics2D graph = source.createGraphics();

int x = (QRCODE_SIZE - width) / 2;

int y = (QRCODE_SIZE - height) / 2;

graph.drawImage(src, x, y, width, height, null);

Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6);

graph.setStroke(new BasicStroke(3f));

graph.draw(shape);

graph.dispose();

}

public static void encode(String content, String imgPath, String destPath, boolean needCompress) throws Exception {

BufferedImage image = createImage(content, imgPath, needCompress);

mkdirs(destPath);

ImageIO.write(image, FORMAT_NAME, new File(destPath));

}

public static BufferedImage encode(String content, String imgPath, boolean needCompress) throws Exception {

BufferedImage image = createImage(content, imgPath, needCompress);

return image;

}

public static void mkdirs(String destPath) {

File file = new File(destPath);

// 当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir.(mkdir如果父目录不存在则会抛出异常)

if (!file.exists() && !file.isDirectory()) {

file.mkdirs();

}

}

public static void encode(String content, String imgPath, String destPath) throws Exception {

encode(content, imgPath, destPath, false);

}

public static void encode(String content, String destPath) throws Exception {

encode(content, null, destPath, false);

}

public static void encode(String content, String imgPath, OutputStream output, boolean needCompress)

throws Exception {

BufferedImage image = createImage(content, imgPath, needCompress);

ImageIO.write(image, FORMAT_NAME, output);

}

public static void encode(String content, OutputStream output) throws Exception {

encode(content, null, output, false);

}

public static String decode(File file) throws Exception {

BufferedImage image;

image = ImageIO.read(file);

if (image == null) {

return null;

}

BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image);

BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));

Result result;

Hashtable hints = new Hashtable();

hints.put(DecodeHintType.CHARACTER_SET, CHARSET);

result = new MultiFormatReader().decode(bitmap, hints);

String resultStr = result.getText();

return resultStr;

}

public static String decode(String path) throws Exception {

return decode(new File(path));

}

}

package com.wyj.alipay.util;

import lombok.Data;

/**

* @author: yijun.wen

* @date: 2022/3/11 5:39 下午

* @description:

*/

@Data

public class QrCodeResponse {

/**

* 返回的状态码

*/

private String code;

/**

* 返回的信息

*/

private String msg;

/**

* 交易的流水号

*/

private String out_trade_no;

/**

* 生成二维码的内容

*/

private String qr_code;

}

package com.wyj.alipay.util;

import lombok.Data;

/**

* @author: yijun.wen

* @date: 2022/3/11 5:38 下午

* @description:

*/

@Data

public class QrResponse {

private QrCodeResponse alipay_trade_precreate_response;

private String sign;

public QrCodeResponse getAlipay_trade_precreate_response() {

return alipay_trade_precreate_response;

}

public void setAlipay_trade_precreate_response(QrCodeResponse alipay_trade_precreate_response) {

this.alipay_trade_precreate_response = alipay_trade_precreate_response;

}

}

3.6 订单号生成工具类

类似雪花只试用单体项目

package com.wyj.alipay.util;

import java.text.SimpleDateFormat;

import java.util.Date;

/**

* @author: yijun.wen

* @date: 2022/3/14 10:23 上午

* @description:

*/

public class GenerateNum {

/**

* 全局自增数

*/

private static int count = 0;

/**

* 每毫秒秒最多生成多少订单(最好是像9999这种准备进位的值)

*/

private static final int total = 99;

/**

* 格式化的时间字符串

*/

private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");

/**

* 获取当前时间年月日时分秒毫秒字符串

*

* @return

*/

private static String getNowDateStr() {

return sdf.format(new Date());

}

/**

* 记录上一次的时间,用来判断是否需要递增全局数

*/

private static String now = null;

/**

* 生成一个订单号

*/

public static String generateOrder() {

String dataStr = getNowDateStr();

if (dataStr.equals(now)) {

count++;

} else {

count = 1;

now = dataStr;

}

// 算补位

int countInteger = String.valueOf(total).length() - String.valueOf(count).length();

//补字符串

String bu = "";

for (int i = 0; i < countInteger; i++) {

bu += "0";

}

bu += String.valueOf(count);

if (count >= total) {

count = 0;

}

return dataStr + bu;

}

}

3.7 项目VO与DTO导入

package com.wyj.alipay.model;

import lombok.Data;

import lombok.NoArgsConstructor;

import java.io.Serializable;

/**

* @author: yijun.wen

* @date: 2022/3/11 5:52 下午

* @description:

*/

@Data

@NoArgsConstructor

public class ViewData implements Serializable {

protected int code;

protected V data;

protected Object error;

}

package com.wyj.alipay.model;

import lombok.Data;

import java.io.Serializable;

/**

* @author: yijun.wen

* @date: 2022/3/11 5:50 下午

* @description:

*/

@Data

public class PayDto implements Serializable {

private Long userId;

private String totalAmount;

private int payType;

}

package com.wyj.alipay.model;

import lombok.Data;

import java.io.Serializable;

/**

* @author: yijun.wen

* @date: 2022/3/11 5:52 下午

* @description:

*/

@Data

public class PayCallbackDto implements Serializable {

private Long userId;

private String payNumber;

}

package com.wyj.alipay.model;

import lombok.Data;

/**

* @author: yijun.wen

* @date: 2022/3/14 10:21 上午

* @description:

*/

@Data

public class QrCodeVo {

private Long UserId;

private String payNumber;

private String qrCode;

}

上述为准备工作,接下来开始核心代码

4、 创建alipay业务接口及实现类

4.1 整体业务接口

package com.wyj.alipay.service;

import com.alipay.api.AlipayApiException;

import com.wyj.alipay.model.PayCallbackDto;

import com.wyj.alipay.model.PayDto;

import com.wyj.alipay.model.ViewData;

import javax.servlet.http.HttpServletRequest;

/**

* @author: yijun.wen

* @date: 2022/3/11 5:44 下午

* @description:

*/

public interface AlipayService {

/**

* 生成支付二维码

*

* @param payInfo

* @return

*/

ViewData alipay(PayDto payInfo);

/**

* 支付宝回调接口

*

* @param request

* @return

*/

boolean alipayCallback(HttpServletRequest request);

/**

* alipay 监听支付状态的接口

*

* @param payCallbackInfo

* @return

* @throws AlipayApiException

*/

ViewData alipayCallback(PayCallbackDto payCallbackInfo) throws AlipayApiException;

}

4.2 二维码返回接口实现

package com.wyj.alipay.service.impl;

import cn.hutool.json.JSONObject;

import cn.hutool.json.JSONUtil;

import com.alibaba.fastjson.JSON;

import com.alipay.api.AlipayApiException;

import com.alipay.api.AlipayClient;

import com.alipay.api.DefaultAlipayClient;

import com.alipay.api.domain.AlipayTradePrecreateModel;

import com.alipay.api.request.AlipayTradePrecreateRequest;

import com.alipay.api.response.AlipayTradePrecreateResponse;

import com.wyj.alipay.config.AlipayConfig;

import com.wyj.alipay.model.PayCallbackDto;

import com.wyj.alipay.model.PayDto;

import com.wyj.alipay.model.QrCodeVo;

import com.wyj.alipay.model.ViewData;

import com.wyj.alipay.service.IAlipayService;

import com.wyj.alipay.util.GenerateNum;

import com.wyj.alipay.util.QrCodeResponse;

import com.wyj.alipay.util.QrCodeUtil;

import com.wyj.alipay.util.QrResponse;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.util.FileCopyUtils;

import javax.imageio.ImageIO;

import javax.imageio.stream.ImageOutputStream;

import javax.servlet.http.HttpServletRequest;

import java.awt.image.BufferedImage;

import java.io.ByteArrayInputStream;

import java.io.ByteArrayOutputStream;

import java.util.Base64;

/**

* @author: yijun.wen

* @date: 2022/3/14 10:19 上午

* @description:

*/

@Service

@Slf4j

public class AlipayServiceImpl implements IAlipayService {

@Autowired

private AlipayConfig alipayConfig;

@Override

public ViewData alipay(PayDto payInfo) {

ViewData viewData = new ViewData<>();

// 1:支付的用户

Long userId = payInfo.getUserId();

// 2: 支付金额

String totalAmount = payInfo.getTotalAmount();

// 3: 支付的产品名称

String productName = "Alipay test";

// 4: 支付的订单编号

String payNumber = GenerateNum.generateOrder();

// 5: 支付方式

int payType = payInfo.getPayType();

// 6:支付宝携带的参数在回调中可以通过request获取 参数

JSONObject json = JSONUtil.createObj();

json.set("memberId", userId);

json.set("totalAmount", totalAmount);

json.set("productName", productName);

json.set("payNumber", payNumber);

json.set("payType", payType);

// 7:设置支付相关的信息

AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();

// 自定义订单号

model.setOutTradeNo(payNumber);

// 支付金额

model.setTotalAmount(totalAmount);

// 支付的产品名称

model.setSubject(productName);

// 支付的请求体参数

model.setBody(json.toString());

// 支付的超时时间

model.setTimeoutExpress("5m");

// 支付的库存 id(根据 cloudPKI 业务,这里我们用用户id )

model.setStoreId(userId + "");

// 调用 alipay 获取二维码参数

QrCodeResponse qrCodeResponse = qrcodePay(model);

try {

ByteArrayOutputStream output = new ByteArrayOutputStream();

// 自定义二维码logo todo: 可以在二维码中间可以加上公司 logo

//String logoPath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath();

String logoPath = "";

// 生成二维码

BufferedImage buffImg = QrCodeUtil.encode(qrCodeResponse.getQr_code(), logoPath, false);

ImageOutputStream imageOut = ImageIO.createImageOutputStream(output);

ImageIO.write(buffImg, "JPEG", imageOut);

imageOut.close();

ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());

byte[] data = FileCopyUtils.copyToByteArray(input);

QrCodeVo qrCodeVo = new QrCodeVo();

qrCodeVo.setQrCode(Base64.getEncoder().encodeToString(data));

qrCodeVo.setPayNumber(payNumber);

qrCodeVo.setUserId(userId);

viewData.setData(qrCodeVo);

return viewData;

} catch (Exception ex) {

ex.printStackTrace();

return viewData;

}

}

@Override

public boolean alipayCallback(HttpServletRequest request) {

return false;

}

@Override

public ViewData alipayCallback(PayCallbackDto payCallbackInfo) throws AlipayApiException {

return null;

}

/**

* 扫码运行代码

* 验签通过返回QrResponse

* 失败打印日志信息

* 参考地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.app.pay

*

* @param model

* @return

*/

public QrCodeResponse qrcodePay(AlipayTradePrecreateModel model) {

// 1: 获取阿里请求客户端

AlipayClient alipayClient = getAlipayClient();

// 2: 获取阿里请求对象

AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();

// 3:设置请求参数的集合,最大长度不限

request.setBizModel(model);

// 设置异步回调地址

request.setNotifyUrl(alipayConfig.getNotify_url());

// 设置同步回调地址

request.setReturnUrl(alipayConfig.getReturn_url());

AlipayTradePrecreateResponse alipayTradePrecreateResponse = null;

try {

alipayTradePrecreateResponse = alipayClient.execute(request);

} catch (AlipayApiException e) {

e.printStackTrace();

}

QrResponse qrResponse = JSON.parseObject(alipayTradePrecreateResponse.getBody(), QrResponse.class);

return qrResponse.getAlipay_trade_precreate_response();

}

/**

* 获取AlipayClient对象

*

* @return

*/

private AlipayClient getAlipayClient() {

//获得初始化的AlipayClient

AlipayClient alipayClient =

new DefaultAlipayClient(alipayConfig.getGatewayUrl(), alipayConfig.getApp_id(), alipayConfig.getMerchant_private_key(),

"JSON", alipayConfig.getCharset(), alipayConfig.getAlipay_public_key(), alipayConfig.getSign_type());

return alipayClient;

}

}

4.3 创建 web 接口

package com.wyj.alipay.controller;

import com.wyj.alipay.model.PayDto;

import com.wyj.alipay.model.ViewData;

import com.wyj.alipay.service.IAlipayService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**

* @author: yijun.wen

* @date: 2022/3/14 10:57 上午

* @description:

*/

@RestController

@RequestMapping("/api/pay/alipay/")

public class AlipayController {

@Autowired

private IAlipayService alipayService;

/**

* 生成支付宝支付二维码

*

* @param payInfo

* @return

*/

@PostMapping("/qr_code")

public ViewData alipay(@RequestBody PayDto payInfo) {

return alipayService.alipay(payInfo);

}

}

4.4 使用 Postman 进行接口测试

qr_Code响应值为 Base64 编码

我们可以简单写个 html 页面来测试

My alipay qr_code

4.5 测试效果

5、扫码后回调接口开发

5.1 支付回调接口实现

重写 AlipayServiceImpl alipayCallback方法

/**

* 支付宝回调

*

* @return

* @throws Exception

*/

@Override

public boolean alipayCallback(HttpServletRequest request) {

// 获取支付宝GET过来反馈信息

Map params = new LinkedHashMap<>();

Map requestParams = request.getParameterMap();

for (Iterator 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] + ",";

}

try {

params.put(name, new String(valueStr.getBytes("ISO-8859-1"), "UTF-8"));

} catch (UnsupportedEncodingException e) {

e.printStackTrace();

}

}

// 计算得出通知验证结果

log.info("1:获取支付宝回传的参数" + params);

try {

// 验签

//RSA2密钥验签

boolean checkV1 = AlipaySignature.rsaCheckV1(params, alipayConfig.alipay_public_key, alipayConfig.charset, alipayConfig.sign_type);

log.info("验签成功");

if (!checkV1) {

log.info("验签失败接口参数:{}", params);

return false;

}

} catch (AlipayApiException e) {

e.printStackTrace();

}

// 返回公共参数

String extparamString = request.getParameter("extra_common_param");

log.info("2:支付宝交易返回的公共参数:{}", extparamString);

String tradeNo = params.get("trade_no");

//交易完成

String body = params.get("body");

log.info("3:【支付宝】交易的参数信息是:{},流水号是:{}", body, tradeNo);

try {

JSONObject bodyJson = new JSONObject(body);

Long userId = bodyJson.getLong("userId");

String payType = bodyJson.getStr("payType");

String payNumber = bodyJson.getStr("payNumber");

log.info("4:【支付宝】交易的参数信息是:payType:{},payNumber:{},userId:{}", payType, payNumber, userId);

// todo 入库充值记录 修改库存等一系列 DB 操作

} catch (Exception ex) {

log.error("支付宝支付出现了异常,流水号是:{}", tradeNo);

ex.printStackTrace();

return false;

}

return true;

}

5.2 创建 web 接口

下面代码拷贝到 AlipayController

检查 alipay 回调地址是否为当前接口地址

/**

* alipay 异步通知

* 参考地址:https://opendocs.alipay.com/support/01ravg

*/

@ResponseBody

@PostMapping("/notifyUrl")

public String notify_url(HttpServletRequest request) {

boolean result = alipayService.alipayCallback(request);

if (result) {

// alipay 规范,请不要修改或删除

return "success";

} else {

// 验证失败

return "fail";

}

}

5.3 查看测试效果

注意:回调接口为 alipay 调用,必须外网能够访问

我这里打包部署到服务器上,给大家看下日志效果

重新按照步骤4生成二维码,支付宝扫码支付成功后,可见日志:

6、Alipay 支付状态查询

这个接口主要是给前端轮询调用获取支付状态使用

当然,还有一种解决方案,使用websocket,在步骤5中直接发送消息通知前端

6.1 监听支付状态的接口实现

重写 AlipayServiceImpl alipayCallback 方法

@Override

public ViewData alipayCallback(PayCallbackDto payCallbackInfo) throws AlipayApiException {

ViewData viewData = new ViewData<>();

// 1: 获取阿里请求客户端

AlipayClient alipayClient = getAlipayClient();

// 2: 获取阿里请求对象

AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();

// 3: 设置业务参数

request.setBizContent(JSONUtil.toJsonStr(JSONUtil.createObj().set("out_trade_no", payCallbackInfo.getPayNumber())));

//通过alipayClient调用API,获得对应的response类

AlipayTradeQueryResponse response = alipayClient.execute(request);

String body = response.getBody();

JSONObject json = new JSONObject(new JSONObject(body).getStr("alipay_trade_query_response"));

if ("10000".equals(json.getStr("code")) && "TRADE_SUCCESS".equals(json.getStr("trade_status"))) {

viewData.setData("success");

} else {

viewData.setData("fail");

}

return viewData;

}

6.2 创建 web 接口

/**

* alipay 监听支付状态的接口

*

* @param PayCallbackInfo

* @return

*/

@PostMapping("/alipaycallback")

public ViewData alipayCallback(@RequestBody PayCallbackDto PayCallbackInfo) throws AlipayApiException {

return alipayService.alipayCallback(PayCallbackInfo);

}

6.3 测试

这里前端轮询逻辑,展示二维码 5 秒后,每3秒调用一次支付查询接口,得到响应success或5分钟后结束调用

📌 相关推荐