当面付包括付款码支付和扫码支付两种收款方式。适用于线下实体店支付、面对面支付、自助售货机等场景。
付款码支付:商家使用扫码枪或其他扫码机具扫描用户出示的付款码,来实现收款。
扫码支付:商家提供收款二维码,由用户通过支付宝扫码支付,来实现收款。
• APPID
• 商家私钥
• 支付宝公钥
• 支付回调地址
• 网关地址
• 加密签名算法RSA2
# 支付宝支付参数配置
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
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;
}
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;
}
}
类似雪花只试用单体项目
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;
}
}
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
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;
}
alipay
业务接口及实现类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;
}
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<QrCodeVo> 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;
}
}
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);
}
}
qr_Code
响应值为 Base64 编码
我们可以简单写个 html 页面来测试
重写 AlipayServiceImpl alipayCallback方法
/\*\*
\* 支付宝回调
\*
\* @return
\* @throws Exception
\*/
@Override
public boolean alipayCallback(HttpServletRequest request) {
// 获取支付宝GET过来反馈信息
Map<String, String> 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;
}
下面代码拷贝到 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";
}
}
注意:回调接口为 alipay 调用,必须外网能够访问
我这里打包部署到服务器上,给大家看下日志效果
重新按照步骤4
生成二维码,支付宝扫码支付成功后,可见日志:
这个接口主要是给前端轮询
调用获取支付状态使用
当然,还有一种解决方案,使用websocket
,在步骤5
中直接发送消息通知前端
重写 AlipayServiceImpl alipayCallback 方法
@Override
public ViewData alipayCallback(PayCallbackDto payCallbackInfo) throws AlipayApiException {
ViewData<Object> 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;
}
/\*\*
\* alipay 监听支付状态的接口
\*
\* @param PayCallbackInfo
\* @return
\*/
@PostMapping("/alipaycallback")
public ViewData alipayCallback(@RequestBody PayCallbackDto PayCallbackInfo) throws AlipayApiException {
return alipayService.alipayCallback(PayCallbackInfo);
}
这里前端轮询
逻辑,展示二维码 5 秒后,每3秒调用一次支付查询接口,得到响应success
或5分钟后结束调用
手机扫一扫
移动阅读更方便
你可能感兴趣的文章