|
预期阅读的官方文档以及工具
- 支付宝开放平台 https://open.alipay.com/platform/home.htm
- 支付宝沙箱模式 https://open.alipay.com/platform/appDaily.htm?tab=info
- 支付宝支付Demo下载 https://gw.alipayobjects.com/os/bmw-prod/a526522f-37a6-4a8e-9abc-d22cf240cbbd.zip
- natapp内网穿透工具官网:https://natapp.cn/
支付宝支付的业务流程图
- 支付过程详细描述:
- 触发场景:
- 当用户确认了订单=>选择支付方式=>点击立即支付,在点击立即支付后发送一个拉起支付请求到我们的服务器http://localhost:8080/alipay/pay,参数一般就是订单的基本信息。
- 服务端响应:
- 服务器端处理用户的下单请求http://localhost:8080/alipay/pay。
- 根据客户端传来的订单信息,计算出每一个商品的单价、订单的总金额等(金额是不应该由客户端直接传的,应该由服务器端计算,更安全),再生成一个订单号。
- 调用支付宝支付接口,订单信息进行上传,成功后支付宝会返回一段html文档,就是大家经常看到的带二维码的PC端支付页面,但是==本文中只获取对应的支付宝二维码信息,然后生成对应支付二维码返回给买家==
- 第六步当买家扫码支付完成以后,支付宝会调用同步回调API接口跳转到一个自定义的成功页面(==这个页面只是告诉用户支付成功了,只做展示用途,并非真正的支付回调==)
- 第七步支付完成以后,支付宝会调用异步回调API接口,将一些参数传给服务端,如交易流水号、订单号,服务端可以根据这些信息查询交易是否真的成功了(第8步),从而执行后续的业务,比如将订单状态变为已支付、给用户增加积分、扣减优惠券……等。
支付宝沙箱环境配置
支付宝有一个供开发者测试使用的沙箱环境,会提供一个沙箱版的支付宝app、一个商家账户、一个买家账户。有了这个,可以让我们跳过商家入驻、企业资质审核等过程,开箱即用。
1、配置沙箱环境
进入到支付宝支付官网,点击“我是开发者”,在新的页面右上角,用你自己的支付宝扫码登录,再点击开发服务中的研发服务:
进入到沙箱环境,这里为我们配置了一个唯一的APPID,以及提供了沙箱环境中的网关。
2、获取应用公钥和应用私钥
对于数据的安全考虑,我们需要生成一份公钥和私钥,公钥提供给支付宝,支付宝对数据进行加密;私钥用于解析支付宝传来的加密数据,由我们自行保管。支付宝开放平台为我们提供了密钥生成工具(开发助手)来对应用的客户端服务端之间的交互进行加密保护。工具主要功能有生成密钥、签名、验签、格式转换、密钥匹配、智能反馈、开放社区。
打开下载好的工具,在以下地方点击“生成密钥”,生成以后会出现一个应用私钥和应用公钥,这两者都需要自己保存,应用公钥需要传到支付宝中去换取支付宝公钥,==支付宝公钥和应用私钥需要在后面发起支付请求的时候使用。==
3、获取支付宝公钥
回到沙箱应用控制台,点击设置公钥(==注意不是公钥证书哦,我们用的不是证书的方式==)
将应用公钥输入到如图所示,获取到支付宝公钥
4、下载沙箱支付宝APP
在沙箱应用控制台,扫码下载沙箱版的支付宝(目前只有安卓版),提供了一个商家号、一个买家号,里面可以自由充值费用,并且该商家,买家账号用于登陆沙箱支付宝APP。
接下来我们进行代码实现!!!
创建数据库表
本案例中创建的表为课程表courses(商品),订单表order_detail
1、 课程表
2、订单表
搭建Springboot项目
1、配置POM文件,其中SDK采用的是alipay-sdk-java 4.16.2.ALL
<?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?>
<project xmlns=&#34;http://maven.apache.org/POM/4.0.0&#34; xmlns:xsi=&#34;http://www.w3.org/2001/XMLSchema-instance&#34;
xsi:schemaLocation=&#34;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&#34;>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lonely</groupId>
<artifactId>alipay_demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>alipay_demo</name>
<description>alipay_demo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
<!--热部署插件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--数据库连接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.3</version>
</dependency>
<!--Mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- alipay-sdk-java -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.16.2.ALL</version>
</dependency>
<!--JSON转换类-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--工具类建议多看-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<!--二维码生成-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<!--加密依赖-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<!--http请求-->
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
<!--配置文件ENC加密处理-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<!--打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>2、项目整体结构介绍以及启动类配置
通过EasyCode工具类,生成课程表和订单表对应的实体类(entity),控制层(controller),服务层(service),以及数据操作层(dao,mapper),其余目录结构如下图所示。后面业务中会说明详细创建过程。
3、配置application.yml文件,配置数据源,日志等信息。
spring:
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/alipay?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
# 启动配置文件
profiles:
active: dev
# 配置时间格式和时差问题
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
locale: zh_CN
# 解决json返回过程中long的精度丢失问题
generator:
write-numbers-as-strings: true
write-bigdecimal-as-plain: true
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
# 解决bean重复定义的
main:
allow-bean-definition-overriding: true
# dispatcherServlet 是懒加载机制,配置为1 表示提前加载
mvc:
servlet:
load-on-startup: 1
# 日志配置管理
logging:
level:
root: info
# mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.lonely.alipay_demo.entity 配置AlipayConfig类
1、 创建AlipauConfig工具类,主要用户配置支付请求创建所需要的公共参数
package com.lonely.alipay_demo.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @Author: xiyang
* @FileName: AlipayConfig
* @Date: Created in 2021/8/5 10:32
* @Vserion:
* @Description: TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Component
public class AlipayConfig {
/**
* 商户appid
*/
@Value(&#34;${alipay.APPID}&#34;)
public String APPID;
/**
* 私钥 pkcs8格式的
*/
@Value(&#34;${alipay.RSA_PRIVATE_KEY}&#34;)
public String RSA_PRIVATE_KEY;
/**
* 服务器异步通知页面路径
* 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
*/
@Value(&#34;${alipay.notify_url}&#34;)
public String notify_url;
/**
* 页面跳转同步通知页面路径
* 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,
* 必须外网可以正常访问 商户可以自定义同步跳转地址
*/
@Value(&#34;${alipay.return_url}&#34;)
public String return_url;
/**
* 请求网关地址
*/
@Value(&#34;${alipay.URL}&#34;)
public String URL;
/**
* 编码
*/
@Value(&#34;${alipay.CHARSET}&#34;)
public String CHARSET;
/**
* 返回格式
*/
@Value(&#34;${alipay.FORMAT}&#34;)
public String FORMAT;
/**
* 支付宝公钥
*/
@Value(&#34;${alipay.ALIPAY_PUBLIC_KEY}&#34;)
public String ALIPAY_PUBLIC_KEY;
/**
* 日志记录目录定义在 logFile 中
*/
@Value(&#34;${alipay.log_path}&#34;)
public String log_path;
/**
* RSA2
*/
@Value(&#34;${alipay.SIGNTYPE}&#34;)
public String SIGNTYPE;
}2、配置application-dev.yml文件,主要配置AlipauConfig工具类的参数值
#服务器端口号配置
server:
port: 8080
# 数据源信息配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/alipay?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
# 配置alipay数据
alipay:
# 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
APPID: 沙箱中的APPID
# 商户私钥,您的PKCS8格式RSA2私钥
RSA_PRIVATE_KEY: 应用私钥
# 支付宝支付公钥
ALIPAY_PUBLIC_KEY: 支付宝公钥(通过应用公钥换取的值)
# 异步回调地址 必须外网能够访问(这里需要配置内网穿透),当支付成功后会调用该API
notify_url: http://hpzekg.natappfree.cc/alipay/pay/callback
# 同步回调地址 必须外网能够访问
return_url:
# 网关(注意沙箱网关和正式网关的区别,这里填写沙箱环境下的网关)
URL: https://openapi.alipaydev.com/gateway.do
# 编码
CHARSET: UTF-8
# 返回数据格式
FORMAT: json
# 日志地址
log_path: /log
# RSA2
SIGNTYPE: RSA2 用户拉起支付请求,获取付款二维码
当买家确认了订单=>选择支付方式=>点击立即支付后发送一个拉起支付请求到我们的服务器http://localhost:8080/alipay/pay,参数一般就是商品的ID和支付方式payType(其余参数大家可以自行添加)。
1、 创建PayVo实体类,用于接收订单参数
package com.lonely.alipay_demo.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author: xiyang
* @FileName: PayVo
* @Date: Created in 2021/8/6 14:45
* @Vserion:
* @Description: TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PayVo {
private String courseId;
private Integer payMethod;
}2、在AlipayController 中编写http://localhost:8080/alipay/pay的API
package com.lonely.alipay_demo.controller;
import com.lonely.alipay_demo.service.AlipayService;
import com.lonely.alipay_demo.vo.PayVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* @Author: xiyang
* @FileName: AlipayController
* @Date: Created in 2021/8/6 12:30
* @Vserion:
* @Description: TODO
*/
@RestController
public class AlipayController {
@Autowired
private AlipayService alipayService;
/**
* 拉起支付请求
* @param payVo
* @return
* @throws Exception
*/
@GetMapping(&#34;/alipay/pay&#34;)
public byte[] alipay(PayVo payVo) throws Exception{
byte[] alipay = alipayService.alipay(payVo);
return alipay;
}
}3、编写拉起支付的服务层接口-->alipay接口
package com.lonely.alipay_demo.service;
import com.lonely.alipay_demo.vo.PayVo;
import javax.servlet.http.HttpServletRequest;
/**
* @Author: xiyang
* @FileName: AlipayService
* @Date: Created in 2021/8/6 12:27
* @Vserion:
* @Description: TODO
*/
public interface AlipayService {
/**
* 获取支付宝支付二维码
* @param payVo
* @return img
*/
byte[] alipay(PayVo payVo) throws Exception;
}4、实现支付二维码业务逻辑-->alipay方法
/**
* @Author: xiyang
* @FileName: AlipayServicImpl
* @Date: Created in 2021/8/6 12:28
* @Vserion:
* @Description: TODO
*/
@Service
public class AlipayServiceImpl implements AlipayService {
Logger logger = LoggerFactory.getLogger(AlipayServiceImpl.class);
@Autowired
private AlipayConfig alipayConfig;
@Resource
private KssCoursesDao kssCoursesDao;
@Override
public byte[] alipay(PayVo payVo) throws Exception {
/**
* 1. 获取阿里客户端
* 2. 获取阿里请求对象
* 3. 设置请求参数
* 4. 设置同步通知回调路径
* 5. 设置异步通知回调路径
*/
KssCourses kssCourses = kssCoursesDao.queryById(payVo.getCourseId());
if (kssCourses == null) {
throw new BusinessException(ResultCodeEnum.SYSTEM_EXCEPTION);
}
String orderNumber = GenerateNum.generateOrder();
//设置支付回调时可以在request中获取的参数
JSONObject jsonObject = new JSONObject();
jsonObject.put(&#34;courseId&#34;, kssCourses.getCourseid());
jsonObject.put(&#34;courseTitle&#34;, kssCourses.getTitle());
jsonObject.put(&#34;courseImg&#34;, kssCourses.getImg());
jsonObject.put(&#34;orderNumber&#34;, orderNumber);
jsonObject.put(&#34;payType&#34;, payVo.getPayMethod());
jsonObject.put(&#34;price&#34;, kssCourses.getPrice());
String params = jsonObject.toString();
//设置支付参数
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
model.setBody(params);
model.setTotalAmount(kssCourses.getPrice().toString());
model.setOutTradeNo(orderNumber);
model.setSubject(kssCourses.getTitle());
//获取响应二维码信息
QrCodeResponse qrCodeResponse = qrcodePay(model);
//制作二维码并且返回给前端
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
String logopath = &#34;&#34;;
logopath = ResourceUtils.getFile(&#34;classpath:favicon.png&#34;).getAbsolutePath();
logger.info(&#34;二维码的图片路径为===>&#34; + logopath);
BufferedImage encode = QRCodeUtil.encode(qrCodeResponse.getQr_code(), logopath, false);
ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(byteArrayOutputStream);
ImageIO.write(encode, &#34;JPEG&#34;, imageOutputStream);
imageOutputStream.close();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
return FileCopyUtils.copyToByteArray(byteArrayInputStream);
}
/**
* 支付宝客户端发送支付请求获取支付二维码信息
*/
public QrCodeResponse qrcodePay(AlipayTradePrecreateModel model) throws AlipayApiException {
//1.获取请求客户端
AlipayClient alipayClient = getAlipayClient();
//2. 获取请求对象
AlipayTradePrecreateRequest alipayRequest = new AlipayTradePrecreateRequest();
//3.设置请求参数
alipayRequest.setBizModel(model);
alipayRequest.setNotifyUrl(alipayConfig.getNotify_url());
alipayRequest.setReturnUrl(alipayConfig.getReturn_url());
AlipayTradePrecreateResponse execute = null;
execute = alipayClient.execute(alipayRequest);
String body = execute.getBody();
logger.info(&#34;请求的响应二维码信息====>&#34; + body);
QrResponse qrResponse = JSON.parseObject(body, QrResponse.class);
return qrResponse.getAlipay_trade_precreate_response();
}
/**
* 获取阿里客户端
*
* @return
*/
public AlipayClient getAlipayClient() {
DefaultAlipayClient defaultAlipayClient = new DefaultAlipayClient(
alipayConfig.getURL(),
alipayConfig.getAPPID(),
alipayConfig.getRSA_PRIVATE_KEY(),
alipayConfig.getFORMAT(),
alipayConfig.getCHARSET(),
alipayConfig.getALIPAY_PUBLIC_KEY(),
alipayConfig.getSIGNTYPE()
);
return defaultAlipayClient;
}
}当请求响应成功以后会给前端返回一个字节数组,为当前订单的支付二维码。效果如下图所示。
5、前端代码如下:
<img
id=&#34;im&#34;
src=&#34;http://localhost:8080/alipay/pay?courseId=1317503462556848129&payMethod=1&#34;
alt=&#34;&#34;
/>6、在业务实现中需要使用到GenerateNum工具类来实现订单号生成--采用雪花算法
package com.lonely.alipay_demo.util;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 根据时间生成随机订单号
*/
public class GenerateNum {
/**
* 全局自增数
*/
private static int count = 0;
/**
* 每毫秒秒最多生成多少订单(最好是像9999这种准备进位的值)
*/
private static final int total = 99;
/**
* 格式化的时间字符串
*/
private static final SimpleDateFormat sdf = new SimpleDateFormat(&#34;yyyyMMddHHmmss&#34;);
/**
* 获取当前时间年月日时分秒毫秒字符串
* @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 = &#34;&#34;;
for (int i = 0; i < countInteger; i++) {
bu += &#34;0&#34;;
}
bu += String.valueOf(count);
if (count >= total) {
count = 0;
}
return dataStr + bu;
}
}7、在向支付宝发起请求后,处理返回结果会使用到QrResponse和QrCodeResponse
QrResponse
package com.lonely.alipay_demo.qrcode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author: xiyang
* @FileName: QrResponse
* @Date: Created in 2021/8/6 19:31
* @Vserion:
* @Description: TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class QrResponse {
private QrCodeResponse alipay_trade_precreate_response;
private String sign;
}QrCodeResponse
package com.lonely.alipay_demo.qrcode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class QrCodeResponse {
/**
* 返回的状态码
*/
private String code;
/**
* 返回的信息
*/
private String msg;
/**
* 交易的流水号
*/
private String out_trade_no;
/**
* 生成二维码的内容
*/
private String qr_code;
}8、在生成字符二维码时,会用到BufferedImageLuminanceSource和 QrCodeUtil工具类
QrCodeUtil
package com.lonely.alipay_demo.qrcode;
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;
public class QRCodeUtil {
private static final String CHARSET = &#34;utf-8&#34;;
private static final String FORMAT_NAME = &#34;JPG&#34;;
// 二维码尺寸
private static final int QRCODE_SIZE = 300;
// LOGO宽度
private static final int WIDTH = 90;
// LOGO高度
private static final int HEIGHT = 90;
/**
* @Author xuke
* @Description 二维码生成的方法
* @Date 0:45 2021/4/2
* @Param [content, imgPath, needCompress]
* @return java.awt.image.BufferedImage
**/
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 || &#34;&#34;.equals(imgPath)) {
return image;
}
// 插入图片
QRCodeUtil.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(&#34;&#34; + imgPath + &#34; 该文件不存在!&#34;);
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 = QRCodeUtil.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 = QRCodeUtil.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 {
QRCodeUtil.encode(content, imgPath, destPath, false);
}
public static void encode(String content, String destPath) throws Exception {
QRCodeUtil.encode(content, null, destPath, false);
}
public static void encode(String content, String imgPath, OutputStream output, boolean needCompress)
throws Exception {
BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress);
ImageIO.write(image, FORMAT_NAME, output);
}
public static void encode(String content, OutputStream output) throws Exception {
QRCodeUtil.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 QRCodeUtil.decode(new File(path));
}
}BufferedImageLuminanceSource
package com.lonely.alipay_demo.qrcode;
import com.google.zxing.LuminanceSource;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
/**
* @author xiyang
*/
public class BufferedImageLuminanceSource extends LuminanceSource {
private final BufferedImage image;
private final int left;
private final int top;
public BufferedImageLuminanceSource(BufferedImage image) {
this(image, 0, 0, image.getWidth(), image.getHeight());
}
public BufferedImageLuminanceSource(BufferedImage image, int left, int top, int width, int height) {
super(width, height);
int sourceWidth = image.getWidth();
int sourceHeight = image.getHeight();
if (left + width > sourceWidth || top + height > sourceHeight) {
throw new IllegalArgumentException(&#34;Crop rectangle does not fit within image data.&#34;);
}
for (int y = top; y < top + height; y++) {
for (int x = left; x < left + width; x++) {
if ((image.getRGB(x, y) & 0xFF000000) == 0) {
image.setRGB(x, y, 0xFFFFFFFF);
}
}
}
this.image = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY);
this.image.getGraphics().drawImage(image, 0, 0, null);
this.left = left;
this.top = top;
}
@Override
public byte[] getRow(int y, byte[] row) {
if (y < 0 || y >= getHeight()) {
throw new IllegalArgumentException(&#34;Requested row is outside the image: &#34; + y);
}
int width = getWidth();
if (row == null || row.length < width) {
row = new byte[width];
}
image.getRaster().getDataElements(left, top + y, width, 1, row);
return row;
}
@Override
public byte[] getMatrix() {
int width = getWidth();
int height = getHeight();
int area = width * height;
byte[] matrix = new byte[area];
image.getRaster().getDataElements(left, top, width, height, matrix);
return matrix;
}
@Override
public boolean isCropSupported() {
return true;
}
@Override
public LuminanceSource crop(int left, int top, int width, int height) {
return new BufferedImageLuminanceSource(image, this.left + left, this.top + top, width, height);
}
@Override
public boolean isRotateSupported() {
return true;
}
@Override
public LuminanceSource rotateCounterClockwise() {
int sourceWidth = image.getWidth();
int sourceHeight = image.getHeight();
AffineTransform transform = new AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth);
BufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = rotatedImage.createGraphics();
g.drawImage(image, transform, null);
g.dispose();
int width = getWidth();
return new BufferedImageLuminanceSource(rotatedImage, top, sourceWidth - (left + width), getHeight(), width);
}
} 内网穿透natapp
回调地址肯定要外网可以访问,要不然支付宝怎么会调用得到呢?但是我们作为开发者可能没有自己到服务器和域名,所以我们使用内网穿透工具natapp获取临时域名。
1、登陆natapp官网,完成注册并且购买免费的web隧道
2、购买后的主页
3、购买后配置隧道
4、下载客户端
5、启动
安装成功后解压安装包 在终端中启动natapp:
./natapp
注意:将natapp放在和config.ini(手动创建)同一个目录,在项目运行时 这个终端窗口不能关闭 一旦关闭 通过以上域名去访问boot项目的资源时会404 找不到该页面,映射完成之后,启动项目,用临时域名测试一下上面的页面
其中congif.ini文件如下,其中authtoken为隧道的token
#将本文件放置于natapp同级目录 程序将读取 [default] 段
#在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置
#命令行参数 -config= 可以指定任意config.ini文件
[default]
authtoken=c3dc61f54e48fbf1 #对应一条隧道的authtoken
clienttoken= #对应客户端的clienttoken,将会忽略authtoken,若无请留空,
log=none #log 日志文件,可指定本地文件, none=不做记录,stdout=直接屏幕输出 ,默认为none
loglevel=ERROR #日志等级 DEBUG, INFO, WARNING, ERROR 默认为 DEBUG
http_proxy= #代理设置 如 http://10.123.10.10:3128 非代理上网用户请务必留空 支付成功后的异步回调,实现订单的添加
当买家使用沙箱支付宝扫码以后,会拉起支付宝付款,当付款成功以后,支付宝会直接调用之前配置的notify_url异步回调API去判定交易信息是否成功,将订单数据添加到数据库中。
异步回调:异步通知是指在请求参数中传入notify_url参数,在用户支付成功后,支付宝服务器会按照这个异步地址使用post方式给notify_url来发送交易信息。可以实现订单状态修改等操作。
同步回调: 是指在请求参数中传入return_url参数,支付成功后跳转到return_url地址后携带的返回参数。只是为了给用户显示支付消息
1、在AlipayController中编写http://localhost:8080/alipay/pay/callback的API,这个API必须返回一个success或者false,否则支付宝会一直重复调用
/**
* 支付成功以后的异步回调API
* @param request
* @return
* @throws Exception
*/
@RequestMapping(&#34;/alipay/pay/callback&#34;)
public String notify_url(HttpServletRequest request) throws Exception{
Boolean result = alipayService.alipayCallback(request);
if(result){
return &#34;success&#34;;
}else{
return &#34;false&#34;;
}
}2、在AlipayService编写alipayCallback接口
/**
* 支付成功后的回调函数
* @param request
* @return
*/
Boolean alipayCallback(HttpServletRequest request) throws Exception;3、在AlipayServiceImpl编写业务实现alipayCallback
@Override
public Boolean alipayCallback(HttpServletRequest request) {
try {
Map<String, String> params = ParamsUtil.ParamstoMap(request);
logger.info(&#34;回调参数=========>&#34; + params);
String trade_no = params.get(&#34;trade_no&#34;);
String body1 = params.get(&#34;body&#34;);
logger.info(&#34;交易的流水号和交易信息===========>&#34;, trade_no, body1);
JSONObject body = JSONObject.parseObject(body1);
//String userId = body.getString(&#34;userId&#34;);
String ptype = body.getString(&#34;payType&#34;);
String orderNumber = body.getString(&#34;orderNumber&#34;);
if (ptype != null && ptype.equals(&#34;1&#34;)) {
payCommonService.payproductcourse(body, &#34;1&#34;, orderNumber, trade_no, &#34;1&#34;);
}
} catch (Exception e) {
e.printStackTrace();
logger.info(&#34;异常====>&#34;, e.toString());
return false;
}
return true;
}4、完成订单添加,在payCommonService编写payproductcourse业务
package com.lonely.alipay_demo.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.lonely.alipay_demo.dao.KssOrderDetailDao;
import com.lonely.alipay_demo.entity.KssOrderDetail;
import com.lonely.alipay_demo.service.PayCommonService;
import com.lonely.alipay_demo.util.GenerateNum;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* @Author: xiyang
* @FileName: payCommonServiceImpl
* @Date: Created in 2021/8/7 10:29
* @Vserion:
* @Description: TODO
*/
@Service
public class PayCommonServiceImpl implements PayCommonService {
@Resource
private KssOrderDetailDao kssOrderDetailDao;
@Transactional(rollbackFor = Exception.class)
@Override
public void payproductcourse(JSONObject body, String userId, String orderNumber, String trade_no, String paymethod) {
// 支付的课程
String courseId = body.getString(&#34;courseId&#34;);
// 支付的金额
String money = body.getString(&#34;price&#34;);
// 保存订单明细表
KssOrderDetail orderDetail = new KssOrderDetail();
orderDetail.setId(Long.valueOf(GenerateNum.generateOrder()));
// orderDetail.setUserid(userId);
orderDetail.setCourseid(courseId);
orderDetail.setUsername(&#34;靓仔&#34;);
orderDetail.setPaymethod(paymethod);
orderDetail.setCoursetitle(body.getString(&#34;courseTitle&#34;));
orderDetail.setOrdernumber(orderNumber);
orderDetail.setTradeno(trade_no);
orderDetail.setPrice(money == null ? &#34;0.01&#34; : money);
kssOrderDetailDao.insert(orderDetail);
}
}5、在这个过程中需要paramsUtil工具类去解析支付宝带用异步回调API的参数
package com.lonely.alipay_demo.util;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* @author xiyang
*/
public class ParamsUtil {
/**
* 将异步通知的参数转化为Map
* @return
*/
public static Map<String, String> ParamstoMap(HttpServletRequest request) throws UnsupportedEncodingException {
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 = &#34;&#34;;
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values : valueStr + values + &#34;,&#34;;
}
// 乱码解决,这段代码在出现乱码时使用。
// valueStr = new String(valueStr.getBytes(&#34;ISO-8859-1&#34;), &#34;utf-8&#34;);
params.put(name, valueStr);
}
return params;
}
}到此为止,整个支付过程已经完成,可以去支付宝的开放平台检查沙箱环境中的卖家账户余额变化
注意事项
1 、可以在前端页面中采用轮询的方式通过商品去查询支付是否成功
2、本文中更多是偏向测试,若有逻辑不完善问题,请大家自行添加和完善
3、源码git地址为:https://github.com/LonelyXy/alipay_demo.git, 源码中删除了application-dev.yml,请自行创建. |
|