前言:在商业开发中,支付业务是必不可少的一个环节。如今支付平台常用微信支付、支付宝支付。相较而言,支付宝支付对接起来比较简单,所以本文以支付宝支付为例。
支付宝提供的沙箱环境,入门门槛非常低,不需要商家认证那一套,这一点对开发者是非常友好的,可以直接在本地运行测试,跟最终的产品上线效果是一样的,好了,废话不说,进入正题。
一、开发流程
二、前景准备
登录支付宝开放平台:https://open.alipay.com/
企业支付宝注册(非必备):https://memberprod.alipay.com/account/reg/index.htm
支付宝支付API:https://opendocs.alipay.com/apis
密钥生成地址:https://miniu.alipay.com/keytool/create
请求参数API:https://opendocs.alipay.com/open/02ekfg?scene=19
01、进入沙箱环境
02、获取参数
- APPID
- 商家私钥
- 商家公钥
- 支付宝公钥
- 支付宝回调地址
- 网关地址
- 加密算法签名RSA2
密钥生成地址:https://miniu.alipay.com/keytool/create
注意: 线上开发获取支付宝公钥的方式为:使用应用公钥去换取支付宝公钥!!!
03、体验测试账户
因为环境为沙箱测试环境,它提供商户号跟买家号,但是该账户只能在支付宝提供的测试APK上使用。
三、PC网页支付实现
01、创建springboot工程
自行创建
02、导入依赖
<!--freemarker模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--test测试类-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--阿里云支付SDK-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.10.192.ALL</version>
</dependency>
<!--二维码生成-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
<!--解析二维码-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.1</version>
</dependency>
<!--加密依赖-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<!--http请求-->
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<!--配置文件加密处理-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
03、配置yml
server:
port: 8081
spring:
freemarker:
suffix: .html
cache: false
## 支付宝支付参数配置
alipay:
app_id: # APPID
merchant_private_key: #支付宝商户私钥
alipay_public_key: #支付宝公钥
notify_url: #服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
return_url: #页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问(如果是二维码扫码可以不配置)
sing_type: RSA2
charset: utf-8
gatewayurl: https://openapi.alipaydev.com/gateway.do # https://openapi.alipay.com/gateway.do 线上地址
# 保存支付日志的地址
log_path: D:/tmp/
04、定义支付配置类 AlipayConfig
package com.qd.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.FileWriter;
import java.io.IOException;
/**
* 支付宝支付配置类
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/27 11:22
*/
@Component
public class AlipayConfig {
// 应用ID
public static String app_id;
// 商户私钥
public static String merchant_private_key;
// 支付宝公钥
public static String alipay_public_key;
// 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
public static String notify_url;
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
public static String return_url;
// 签名方式
public static String sign_type;
// 字符编码格式
public static String charset;
// 支付宝网关
public static String gatewayUrl;
// 日志地址
public static String log_path;
/**
* 写日志,方便测试(看网站需求,也可以改成把记录存入数据库)
*/
public static void logResult(String sWord) {
FileWriter writer = null;
try {
writer = new FileWriter(log_path + "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();
}
}
}
}
/*** 配置文件静态属性注入*/
@Value("${alipay.app_id}")
public void setApp_id(String app_id) {this.app_id = app_id;}
@Value("${alipay.merchant_private_key}")
public void setMerchant_private_key(String merchant_private_key) {this.merchant_private_key = merchant_private_key;}
@Value("${alipay.alipay_public_key}")
public void setAlipay_public_key(String alipay_public_key) {this.alipay_public_key = alipay_public_key;}
@Value("${alipay.notify_url}")
public void setNotify_url(String notify_url) {this.notify_url = notify_url;}
@Value("${alipay.return_url}")
public void setReturn_url(String return_url) {this.return_url = return_url;}
@Value("${alipay.sing_type}")
public void setSign_type(String sign_type) {this.sign_type = sign_type;}
@Value("${alipay.charset}")
public void setCharset(String charset) { this.charset = charset;}
@Value("${alipay.gatewayurl}")
public void setGatewayUrl(String gatewayUrl) {this.gatewayUrl = gatewayUrl;}
@Value("${alipay.log_path}")
public void setLog_path(String log_path) {this.log_path = log_path;}
}
notify_url:异步通知,当你支付成功时,支付宝回调你本地启动项目的接口,必须是外网可以访问的,否则支付宝请求不到,但是又得是你本地的方法「不讨论线上测试」可以使用工具实现内网穿透
05、支付宝支付工具类
package com.qd.controller;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.internal.util.AlipaySignature;
import com.qd.config.AlipayConfig;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* 支付宝支付工具类
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/27 11:25
*/
@Slf4j
public class AliPayUtil {
/*** 初始化AlipayClient*/
public static AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type);
/*** 转码*/
public static String getByte(String param) {
try {
return new String(param.getBytes("ISO-8859-1"), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
/*** 校验签名*/
public static boolean rsaCheckV1(HttpServletRequest request) {
// https://docs.open.alipay.com/54/106370
// 获取支付宝POST过来反馈信息
Map<String, String> params = new HashMap<>();
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] + ",";
}
params.put(name, valueStr);
}
try {
boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type);
return signVerified;
} catch (AlipayApiException e) {
log.debug("verify sigin error, exception is:{}", e);
return false;
}
}
}
06、参数封装成实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order implements java.io.Serializable{
// 商品订单号 必填
private String out_trade_no;;
// 订单名称 必填
private String subject;
// 付款金额 必填
private String total_amount;
// 商品描述
private String description;
// 超时时间
private String timeout_express = "10m";
// 产品编号 必填
private String product_code = "FAST_INSTANT_TRADE_PAY";
}
07、AliController(自行拆分service)
@Slf4j
@Controller
public class AliController {
@ResponseBody
@RequestMapping("/paySuccess")
public String paySuccess() {
return "支付成功!";
}
@ResponseBody
@RequestMapping("/payFail")
public String payFail() {
return "支付失败!";
}
/*** 调用支付*/
@RequestMapping(value = "/goAlipay", produces = "text/html; charset=UTF-8")
@ResponseBody
public String goAlipay(String subject, String totalAmount, String description) throws IOException, AlipayApiException {
// 获得初始化的AlipayClient
AlipayClient alipayClient = AliPayUtil.alipayClient;
// 订单模型测试
//AlipayTradePagePayModel model = new AlipayTradePagePayModel();
//model.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
//model.setSubject("subject:120元");
//model.setTotalAmount("120");
//model.setBody("这是一个description");
//model.setProductCode("FAST_INSTANT_TRADE_PAY");
//AlipayTradePagePayRequest pagePayRequest = new AlipayTradePagePayRequest();
//pagePayRequest.setReturnUrl(AlipayConfig.return_url);
//pagePayRequest.setNotifyUrl(AlipayConfig.notify_url);
//pagePayRequest.setBizModel(model);
// 1:设置参数
Order order = new Order();
order.setOut_trade_no(String.valueOf(System.currentTimeMillis()));
order.setSubject("subject");
order.setTotal_amount("120.00");
order.setDescription("这是一个description");
// 2:设置请求参数
AlipayTradePagePayRequest pagePayRequest = new AlipayTradePagePayRequest();
// 3:设置页面跳转同步通知页面路径
pagePayRequest.setReturnUrl(AlipayConfig.return_url);
// 4:设置服务器异步通知路径
pagePayRequest.setNotifyUrl(AlipayConfig.notify_url);
// 5:以json格式封装数据
pagePayRequest.setBizContent(new ObjectMapper().writeValueAsString(order));
// 请求
String result = alipayClient.pageExecute(pagePayRequest).getBody();
return result;
}
/*** 同步回调*/
@RequestMapping("/return_url")
public ModelAndView return_url(HttpServletResponse response, HttpServletRequest request) throws IOException, AlipayApiException {
log.info(">>>>>>>>支付成功, 进入同步通知接口...");
boolean verifyResult = AliPayUtil.rsaCheckV1(request);
ModelAndView mv = null;
if (verifyResult) {
//商户订单号
String out_trade_no = AliPayUtil.getByte(request.getParameter("out_trade_no"));
//支付宝交易号
String trade_no = AliPayUtil.getByte(request.getParameter("trade_no"));
log.info("商户订单号:{},支付宝交易号,{}", out_trade_no, trade_no);
mv = new ModelAndView("paySuccess");
} else {
mv = new ModelAndView("payFail");
}
return mv;
}
/*** 异步回调*/
@ResponseBody
@RequestMapping(value = "/notify_url", method = RequestMethod.POST)
public String notify_url(HttpServletResponse response, HttpServletRequest request) throws IOException, AlipayApiException {
log.info(">>>>>>>>支付成功, 进入异步通知接口...");
// 一定要验签,防止黑客篡改参数
Map<String, String[]> parameterMap = request.getParameterMap();
StringBuilder notifyBuild = new StringBuilder(">>>>>>>>>> alipay notify >>>>>>>>>>>>>>\n");
parameterMap.forEach((key, value) -> notifyBuild.append(key + "=" + value[0] + "\n"));
notifyBuild.append(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
log.info(notifyBuild.toString());
boolean flag = AliPayUtil.rsaCheckV1(request);
if (flag) {
/**
* TODO 需要严格按照如下描述校验通知数据的正确性
*
* 商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,
* 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),
* 同时需要校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email),
*
* 上述有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。
* 在上述验证通过后商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。
* 在支付宝的业务通知中,只有交易通知状态为TRADE_SUCCESS或TRADE_FINISHED时,支付宝才会认定为买家付款成功。
*/
//交易状态
String tradeStatus = AliPayUtil.getByte(request.getParameter("trade_status"));
// 商户订单号
String out_trade_no = AliPayUtil.getByte(request.getParameter("out_trade_no"));
//支付宝交易号
String trade_no = AliPayUtil.getByte(request.getParameter("trade_no"));
//付款金额
String total_amount = AliPayUtil.getByte(request.getParameter("total_amount"));
log.info("交易状态:{},商户订单号:{},支付宝交易号:{},付款金额:{}", tradeStatus, out_trade_no, trade_no, total_amount);
// TRADE_FINISHED(表示交易已经成功结束,并不能再对该交易做后续操作);
// TRADE_SUCCESS(表示交易已经成功结束,可以对该交易做后续操作,如:分润、退款等);
if (tradeStatus.equals("TRADE_FINISHED")) {
//判断该笔订单是否在商户网站中已经做过处理
//如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,
// 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序
//请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
//如果有做过处理,不执行商户的业务程序
//注意:
//如果签约的是可退款协议,退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知
//如果没有签约可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。
} else if (tradeStatus.equals("TRADE_SUCCESS")) {
//判断该笔订单是否在商户网站中已经做过处理
//如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,
// 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序
//请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
//如果有做过处理,不执行商户的业务程序
//注意:
//如果签约的是可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。
}
return "success";
}
return "fail";
}
/***查看支付流水*/
@RequestMapping(value = "/queryPay")
@ResponseBody
public String queryPay(String orderId) throws IOException, AlipayApiException {
AlipayClient alipayClient = AliPayUtil.alipayClient;
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(orderId);
//设置请求参数
AlipayTradeQueryRequest alipayRequest = new AlipayTradeQueryRequest();
alipayRequest.setBizModel(model);
//请求
String result = alipayClient.execute(alipayRequest).getBody();
return result;
}
/**
* 退款
*
* @param orderNo 商户订单号
* @return
*/
@PostMapping("/refund")
@ResponseBody
public String refund(String orderNo) throws AlipayApiException {
AlipayTradeRefundRequest alipayRequest = new AlipayTradeRefundRequest();
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
// 商户订单号
model.setOutTradeNo(orderNo);
// 退款金额
model.setRefundAmount("0.1");
// 退款原因
model.setRefundReason("无理由退货");
// 退款订单号(同一个订单可以分多次部分退款,当分多次时必传)
String outOrderId = UUID.randomUUID().toString();
model.setOutRequestNo(outOrderId);
log.info("退款请求号:{}", outOrderId);
alipayRequest.setBizModel(model);
AlipayTradeRefundResponse alipayResponse = AliPayUtil.alipayClient.execute(alipayRequest);
return alipayResponse.getBody();
}
/**
* 退款查询
*
* @param orderNo 商户订单号
* @param refundOrderNo 请求退款接口时,传入的退款请求号,如果在退款请求时未传入,则该值为创建交易时的外部订单号
* @return
* @throws AlipayApiException
*/
@GetMapping("/refundQuery")
@ResponseBody
public String refundQuery(String orderNo, String refundOrderNo) throws AlipayApiException {
AlipayTradeFastpayRefundQueryRequest alipayRequest = new AlipayTradeFastpayRefundQueryRequest();
AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
model.setOutTradeNo(orderNo);
model.setOutRequestNo(refundOrderNo);
alipayRequest.setBizModel(model);
AlipayTradeFastpayRefundQueryResponse alipayResponse = AliPayUtil.alipayClient.execute(alipayRequest);
System.out.println(alipayResponse.getBody());
return alipayResponse.getBody();
}
/**
* 关闭交易
*
* @param orderNo
* @return
* @throws AlipayApiException
*/
@PostMapping("/close")
@ResponseBody
public String close(String orderNo) throws AlipayApiException {
AlipayTradeCloseRequest alipayRequest = new AlipayTradeCloseRequest();
AlipayTradeCloseModel model = new AlipayTradeCloseModel();
model.setOutTradeNo(orderNo);
alipayRequest.setBizModel(model);
AlipayTradeCloseResponse alipayResponse = AliPayUtil.alipayClient.execute(alipayRequest);
System.out.println(alipayResponse.getBody());
return alipayResponse.getBody();
}
}
08、运行结果
四、二维码当面付实现
01、创建springboot项目
02、导入依赖
03、定义支付配置类 AlipayConfig
04、支付宝支付工具类
这些步骤与前面一样,请看上
05、配置yml文件
server:
port: 8081
spring:
freemarker:
suffix: .html
cache: false
## 支付宝支付参数配置
alipay:
app_id: # APPID
merchant_private_key: #支付宝商户私钥
alipay_public_key: #支付宝公钥
notify_url: #服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
return_url: #页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问(如果是二维码扫码可以不配置)
sing_type: RSA2
charset: utf-8
gatewayurl: https://openapi.alipaydev.com/gateway.do # https://openapi.alipay.com/gateway.do 线上地址
# 保存支付日志的地址
log_path: D:/tmp/
## 支付二维码配置
qrcode:
# 二维码logo地址
logo_location: D:\tmp\yjtp.png
# 生成的二维码地址
file_path: D:\tmp\pay\
06、生成二维码工具类
package com.qd.utils;
import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.bouncycastle.util.encoders.Base64Encoder;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Hashtable;
import java.util.Random;
/**
* 二维码生成工具类
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/27 19:05
*/
public class QRCodeUtil {
private static final String CHARSET = "utf-8";
private static final String FORMAT = "JPG";
// 二维码尺寸
private static final int QRCODE_SIZE = 300;
// LOGO宽度
private static final int LOGO_WIDTH = 60;
// LOGO高度
private static final int LOGO_HEIGHT = 60;
private static BufferedImage createImage(String content, String logoPath, boolean needCompress) throws Exception {
Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
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 (logoPath == null || "".equals(logoPath)) {
return image;
}
// 插入图片
QRCodeUtil.insertImage(image, logoPath, needCompress);
return image;
}
/**
* 插入LOGO
*
* @param source 二维码图片
* @param logoPath LOGO图片地址
* @param needCompress 是否压缩
* @throws Exception
*/
private static void insertImage(BufferedImage source, String logoPath, boolean needCompress) throws Exception {
File file = new File(logoPath);
if (!file.exists()) {
throw new Exception("logo file not found.");
}
Image src = ImageIO.read(new File(logoPath));
int width = src.getWidth(null);
int height = src.getHeight(null);
if (needCompress) { // 压缩LOGO
if (width > LOGO_WIDTH) {
width = LOGO_WIDTH;
}
if (height > LOGO_HEIGHT) {
height = LOGO_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();
}
/**
* 生成二维码(内嵌LOGO)
* 二维码文件名随机,文件名可能会有重复
*
* @param content 内容
* @param logoPath LOGO地址
* @param destPath 存放目录
* @param needCompress 是否压缩LOGO
* @throws Exception
*/
public static String encode(String content, String logoPath, String destPath, boolean needCompress) throws Exception {
BufferedImage image = QRCodeUtil.createImage(content, logoPath, needCompress);
mkdirs(destPath);
String fileName = System.currentTimeMillis() + new Random().nextInt(99999999) + "." + FORMAT.toLowerCase();
ImageIO.write(image, FORMAT, new File(destPath + "/" + fileName));
return fileName;
}
/**
* 生成二维码(内嵌LOGO)
* 调用者指定二维码文件名
*
* @param content 内容
* @param logoPath LOGO地址
* @param destPath 存放目录
* @param fileName 二维码文件名
* @param needCompress 是否压缩LOGO
* @throws Exception
*/
public static String encode(String content, String logoPath, String destPath, String fileName, boolean needCompress) throws Exception {
BufferedImage image = QRCodeUtil.createImage(content, logoPath, needCompress);
mkdirs(destPath);
fileName = fileName.substring(0, fileName.indexOf(".") > 0 ? fileName.indexOf(".") : fileName.length())
+ "." + FORMAT.toLowerCase();
ImageIO.write(image, FORMAT, new File(destPath + "/" + fileName));
return fileName;
}
/**
* 当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir.
* (mkdir如果父目录不存在则会抛出异常)
*
* @param destPath 存放目录
*/
public static void mkdirs(String destPath) {
File file = new File(destPath);
if (!file.exists() && !file.isDirectory()) {
file.mkdirs();
}
}
/**
* 生成二维码(内嵌LOGO)
*
* @param content 内容
* @param logoPath LOGO地址
* @param destPath 存储地址
* @throws Exception
*/
public static String encode(String content, String logoPath, String destPath) throws Exception {
return QRCodeUtil.encode(content, logoPath, destPath, false);
}
/**
* 生成二维码
*
* @param content 内容
* @param destPath 存储地址
* @param needCompress 是否压缩LOGO
* @throws Exception
*/
public static String encode(String content, String destPath, boolean needCompress) throws Exception {
return QRCodeUtil.encode(content, null, destPath, needCompress);
}
/**
* 生成二维码
*
* @param content 内容
* @param destPath 存储地址
* @throws Exception
*/
public static String encode(String content, String destPath) throws Exception {
return QRCodeUtil.encode(content, null, destPath, false);
}
/**
* 生成二维码(内嵌LOGO)
*
* @param content 内容
* @param logoPath LOGO地址
* @param output 输出流
* @param needCompress 是否压缩LOGO
* @throws Exception
*/
public static void encode(String content, String logoPath, OutputStream output, boolean needCompress)
throws Exception {
BufferedImage image = QRCodeUtil.createImage(content, logoPath, needCompress);
ImageIO.write(image, FORMAT, output);
}
/**
* 生成二维码
*
* @param content 内容
* @param output 输出流
* @throws Exception
*/
public static void encode(String content, OutputStream output) throws Exception {
QRCodeUtil.encode(content, null, output, false);
}
/**
* 解析二维码
*
* @param file 二维码图片
* @return
* @throws Exception
*/
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<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>();
hints.put(DecodeHintType.CHARACTER_SET, CHARSET);
result = new MultiFormatReader().decode(bitmap, hints);
String resultStr = result.getText();
return resultStr;
}
/**
* 解析二维码
*
* @param path 二维码图片地址
* @return
* @throws Exception
*/
public static String decode(String path) throws Exception {
return QRCodeUtil.decode(new File(path));
}
/**
* 二维码图片转Base64
*
* @param imagePath 图片路径
* @return
*/
public static String imageToBase64(String imagePath) throws IOException {
// 获取文件流
FileInputStream inputStream = new FileInputStream(imagePath);
// 转成字节
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
inputStream.close();
// 对字符数组进行base64编码
BASE64Encoder base64Encoder = new BASE64Encoder();
return base64Encoder.encode(bytes);
}
public static void main(String[] args) throws Exception {
String text = "http://www.baidu.com";
//不含Logo
//QRCodeUtil.encode(text, null, "e:\\", true);
//含Logo,不指定二维码图片名
//QRCodeUtil.encode(text, "e:\\csdn.jpg", "e:\\", true);
//含Logo,指定二维码图片名
String qrcode = QRCodeUtil.encode(text, "D:\\qdSystem\\Resources\\Pictures\\Saved Pictures\\yjtp.png", "D:\\tmp", true);
System.out.println(qrcode);
String decode = QRCodeUtil.decode("D:\\tmp\\qrcode.jpg");
System.out.println(decode);
}
}
07、参数封装成实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order implements java.io.Serializable {
// 商品订单号 必填
private String out_trade_no;
// 订单名称 必填
private String subject;
// 付款金额 必填
private String total_amount;
// 商品描述
private String description;
// 超时时间
private String timeout_express = "10m";
// 产品编号 必填
private String product_code = "FACE_TO_FACE_PAYMENT"; // 二维码当面支付填这个 FACE_TO_FACE_PAYMENT PC:FAST_INSTANT_TRADE_PAY
}
08、AliQrController(自行拆分service)
/**
* 支付宝二维码支付demo
*/
@Slf4j
@Controller
@RequestMapping("/qr")
public class AliQrController {
@Value("${qrcode.logo_location}")
private String logoLocation;
@Value("${qrcode.file_path}")
private String filePath;
@ResponseBody
@RequestMapping("/paySuccess")
public String paySuccess() {
return "支付成功!";
}
@ResponseBody
@RequestMapping("/payFail")
public String payFail() {
return "支付失败!";
}
/*** 调用支付*/
@RequestMapping(value = "/goAlipay", produces = "text/html; charset=UTF-8")
@ResponseBody
public String goAlipay(String subject, String totalAmount, String description) throws IOException, AlipayApiException {
// 获得初始化的AlipayClient
AlipayClient alipayClient = AliPayUtil.alipayClient;
// 订单模型测试
//AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
//model.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
//model.setSubject("subject:120元");
//model.setTotalAmount("120");
//model.setBody("这是一个description");
//model.setProductCode("FACE_TO_FACE_PAYMENT");
//model.setTimeoutExpress("10m");
//AlipayTradePrecreateRequest pagePayRequest = new AlipayTradePrecreateRequest();
//pagePayRequest.setReturnUrl(AlipayConfig.return_url);
//pagePayRequest.setNotifyUrl(AlipayConfig.notify_url);
//pagePayRequest.setBizModel(model);
1:设置参数
Order order = new Order();
order.setOut_trade_no(String.valueOf(System.currentTimeMillis()));
order.setSubject("subject");
order.setTotal_amount("120.00");
order.setDescription("这是一个description");
//2:设置请求参数
AlipayTradePrecreateRequest pagePayRequest = new AlipayTradePrecreateRequest();
// 3:设置页面跳转同步通知页面路径
pagePayRequest.setReturnUrl(AlipayConfig.return_url);
// 4:设置服务器异步通知路径
pagePayRequest.setNotifyUrl(AlipayConfig.notify_url);
// 5:以json格式封装数据
pagePayRequest.setBizContent(JSON.toJSONString(order));
//请求
AlipayTradePrecreateResponse response = alipayClient.execute(pagePayRequest);
// 二维码base64编码内容
String base64Image = "";
// 判断是否调用成功
if (response.isSuccess()) {
JSONObject jsonObject = JSON.parseObject(response.getBody());
JSONObject rsj = (JSONObject) jsonObject.get("alipay_trade_precreate_response");
// 生成的二维码地址内容
String qr_code = (String) rsj.get("qr_code");
// 生成二维码
try {
// 支付宝返回内容 logo地址 二维码生成地址 是否压缩
String encode = QRCodeUtil.encode(qr_code, logoLocation, filePath, true);
log.info("工具生成的二维码地址内容------->{}", encode);
base64Image = QRCodeUtil.imageToBase64(filePath + encode);
} catch (Exception e) {
e.printStackTrace();
}
}
log.info("支付宝返回内容------->{}", response.getBody());
// 返回base64结果 前端自行处理
return base64Image;
}
/*** 同步回调*/
@RequestMapping("/return_url")
public ModelAndView return_url(HttpServletResponse response, HttpServletRequest request) throws IOException, AlipayApiException {
log.info(">>>>>>>>支付成功, 进入同步通知接口...");
boolean verifyResult = AliPayUtil.rsaCheckV1(request);
ModelAndView mv = null;
if (verifyResult) {
//商户订单号
String out_trade_no = AliPayUtil.getByte(request.getParameter("out_trade_no"));
//支付宝交易号
String trade_no = AliPayUtil.getByte(request.getParameter("trade_no"));
log.info("商户订单号:{},支付宝交易号,{}", out_trade_no, trade_no);
mv = new ModelAndView("/qr/paySuccess");
} else {
mv = new ModelAndView("/qr/payFail");
}
return mv;
}
/*** 异步回调*/
@ResponseBody
@RequestMapping(value = "/notify_url", method = RequestMethod.POST)
public String notify_url(HttpServletResponse response, HttpServletRequest request) throws IOException, AlipayApiException {
log.info(">>>>>>>>支付成功, 进入异步通知接口...");
// 一定要验签,防止黑客篡改参数
Map<String, String[]> parameterMap = request.getParameterMap();
StringBuilder notifyBuild = new StringBuilder(">>>>>>>>>> alipay notify >>>>>>>>>>>>>>\n");
parameterMap.forEach((key, value) -> notifyBuild.append(key + "=" + value[0] + "\n"));
notifyBuild.append(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
log.info(notifyBuild.toString());
boolean flag = AliPayUtil.rsaCheckV1(request);
if (flag) {
/**
* TODO 需要严格按照如下描述校验通知数据的正确性
*
* 商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,
* 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),
* 同时需要校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email),
*
* 上述有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。
* 在上述验证通过后商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。
* 在支付宝的业务通知中,只有交易通知状态为TRADE_SUCCESS或TRADE_FINISHED时,支付宝才会认定为买家付款成功。
*/
//交易状态
String tradeStatus = AliPayUtil.getByte(request.getParameter("trade_status"));
// 商户订单号
String out_trade_no = AliPayUtil.getByte(request.getParameter("out_trade_no"));
//支付宝交易号
String trade_no = AliPayUtil.getByte(request.getParameter("trade_no"));
//付款金额
String total_amount = AliPayUtil.getByte(request.getParameter("total_amount"));
log.info("交易状态:{},商户订单号:{},支付宝交易号:{},付款金额:{}", tradeStatus, out_trade_no, trade_no, total_amount);
// TRADE_FINISHED(表示交易已经成功结束,并不能再对该交易做后续操作);
// TRADE_SUCCESS(表示交易已经成功结束,可以对该交易做后续操作,如:分润、退款等);
if (tradeStatus.equals("TRADE_FINISHED")) {
//判断该笔订单是否在商户网站中已经做过处理
//如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,
// 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序
//请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
//如果有做过处理,不执行商户的业务程序
//注意:
//如果签约的是可退款协议,退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知
//如果没有签约可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。
} else if (tradeStatus.equals("TRADE_SUCCESS")) {
//判断该笔订单是否在商户网站中已经做过处理
//如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,
// 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序
//请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
//如果有做过处理,不执行商户的业务程序
//注意:
//如果签约的是可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。
}
return "success";
}
return "fail";
}
/***查看支付流水*/
@RequestMapping(value = "/queryPay")
@ResponseBody
public String queryPay(String orderId) throws IOException, AlipayApiException {
AlipayClient alipayClient = AliPayUtil.alipayClient;
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(orderId);
//设置请求参数
AlipayTradeQueryRequest alipayRequest = new AlipayTradeQueryRequest();
alipayRequest.setBizModel(model);
//请求
String result = alipayClient.execute(alipayRequest).getBody();
return result;
}
/**
* 退款
*
* @param orderNo 商户订单号
* @return
*/
@PostMapping("/refund")
@ResponseBody
public String refund(String orderNo) throws AlipayApiException {
AlipayTradeRefundRequest alipayRequest = new AlipayTradeRefundRequest();
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
// 商户订单号
model.setOutTradeNo(orderNo);
// 退款金额
model.setRefundAmount("0.1");
// 退款原因
model.setRefundReason("无理由退货");
// 退款订单号(同一个订单可以分多次部分退款,当分多次时必传)
String outOrderId = UUID.randomUUID().toString();
model.setOutRequestNo(outOrderId);
log.info("退款请求号:{}", outOrderId);
alipayRequest.setBizModel(model);
AlipayTradeRefundResponse alipayResponse = AliPayUtil.alipayClient.execute(alipayRequest);
return alipayResponse.getBody();
}
/**
* 退款查询
*
* @param orderNo 商户订单号
* @param refundOrderNo 请求退款接口时,传入的退款请求号,如果在退款请求时未传入,则该值为创建交易时的外部订单号
* @return
* @throws AlipayApiException
*/
@GetMapping("/refundQuery")
@ResponseBody
public String refundQuery(String orderNo, String refundOrderNo) throws AlipayApiException {
AlipayTradeFastpayRefundQueryRequest alipayRequest = new AlipayTradeFastpayRefundQueryRequest();
AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
model.setOutTradeNo(orderNo);
model.setOutRequestNo(refundOrderNo);
alipayRequest.setBizModel(model);
AlipayTradeFastpayRefundQueryResponse alipayResponse = AliPayUtil.alipayClient.execute(alipayRequest);
System.out.println(alipayResponse.getBody());
return alipayResponse.getBody();
}
/**
* 关闭交易
*
* @param orderNo
* @return
* @throws AlipayApiException
*/
@PostMapping("/close")
@ResponseBody
public String close(String orderNo) throws AlipayApiException {
AlipayTradeCloseRequest alipayRequest = new AlipayTradeCloseRequest();
AlipayTradeCloseModel model = new AlipayTradeCloseModel();
model.setOutTradeNo(orderNo);
alipayRequest.setBizModel(model);
AlipayTradeCloseResponse alipayResponse = AliPayUtil.alipayClient.execute(alipayRequest);
System.out.println(alipayResponse.getBody());
return alipayResponse.getBody();
}
}
09、访问测试
tip:为了数据更加的安全,可以加密部分数据,加密部分本文暂未涉及
五、yml文件加密
前言:在实际的项目开发中,在我们的配置文件中会有隐私数不想以明文方式展现,所以要加密yml文件
01、导入依赖
<!--配置文件加密处理-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
02、编写测试类
@SpringBootTest
public class JasypTest {
// 加密测试
@Test
public void encryptTest() {
StandardPBEStringEncryptor standardPBEBigDecimalEncryptor = new StandardPBEStringEncryptor();
EnvironmentPBEConfig config = new EnvironmentPBEConfig();
config.setPassword("fsdfs8979!~#!@#rewr"); //密钥 不要告诉别人!!!
standardPBEBigDecimalEncryptor.setConfig(config);
String plainText = "hell,qiandu"; // 要加密的内容
String encryptText = standardPBEBigDecimalEncryptor.encrypt(plainText);
System.out.println("加密结果---->" + encryptText);
}
// 解密测试
@Test
public void decTest() {
StandardPBEStringEncryptor standardPBEBigDecimalEncryptor = new StandardPBEStringEncryptor();
EnvironmentPBEConfig config = new EnvironmentPBEConfig();
config.setPassword("fsdfs8979!~#!@#rewr"); //密钥 不要告诉别人!!!
standardPBEBigDecimalEncryptor.setConfig(config);
String encryptText = "6fJIuViOKsNpxPk8ce6CAUiIftXuKb9x"; // 要解密的内容
String plainText = standardPBEBigDecimalEncryptor.decrypt(encryptText);
System.out.println("解密结果---->" + plainText);
}
}
03、启动类上加入注解
@SpringBootApplication
@EnableEncryptableProperties //开启加密注解
public class AlipayApplication {
public static void main(String[] args) {
SpringApplication.run(AlipayApplication.class, args);
}
}
04、在yml中配置(记得作隔离!)
## jasypt配置
jasypt:
encryptor:
# 密钥
password: #你的密钥
结束~
示例代码:https://chenyu6666.lanzoui.com/iK7qewz1o3a
参考文章:https://blog.csdn.net/qq_40673786/article/details/90260736