文章目录
😹 作者: gh-xiaohe
😻 gh-xiaohe的博客
😽 觉得博主文章写的不错的话,希望大家三连(✌关注,✌点赞,✌评论),多多支持一下!!!
🚏 支付宝支付
🚀 支付宝介绍
🚬 1、支付宝平台
官网链接:https://opendocs.alipay.com/common/02fwvj
支付宝开放平台将强⼤的支付、营销、数据能力,通过接口等形式开放给自研商家与服务商(ISV),帮助商家创建更具竞争力的应用。还可协助商家进行推⼴营销。
商家接入开放平台后,基于支付宝海量用户,可以获得更多的流量、用户和收益,同时用户通过商家提供的服务获得了更丰富的体验,平台生态更加繁荣,最终实现多方共赢。
开发者是开放生态的主要组成部分,通过与商家、消费者的合作,提升商家的服务效率和营收,降低运营成本,使消费者的体验更便捷、更愉悦、更完美。无论是服务商开发者还是商家,都能基于开放平台找到适合自己的角色和方向。
通过平台能做什么?
开发者是开放生态的主要组成部分,通过与商家、消费者的合作,提升商家的服务
效率和营收,降低运营成本,使消费者的体验更便捷、更愉悦、更完美。无论是服务商开发者还是商家,都能基于开放平台找到适合自己的角色和方向。
1、自研商家
拥有研发能力的商家,可通过开放能力接口,将支付宝提供的各项功能集成至自身系统中。定制化开发,丰富服务范围,提升用户体验及自身竞争力。
2、服务商(ISV)
ISV 不仅可以进行自研开发,还能为用户提供小程序、生活号、网页、移动等应用,接入平台提供的支付、营销、数据等开放能力,为用户提供系统服务。
插件提供商
通过支付宝提供的丰富的 API 接口,开发者可以开发并在应用市场上线各类插件,
解决⻔店管理、支付核销、会员营销、数据分析等方面的问题。开放平台还为各种应用提供了清晰的盈利模式,通过应用市场的销售获得回报。
服务提供商
在应用市场之外,ISV 还可以通过服务市场为商家提供店铺装修、拍摄修图、地面推⼴、⻔店代运营等服务。实现线上订购,线下服务。
场景分销商
第三方APP 或媒介可以借助支付宝开放平台的分销能力为商家提供基于⻔店、卡
券、内容的分销服务,为商家的流量导入、品牌推⼴提供平台。分销商可以通过所
提供的服务获取收入。
🚬 2、支付宝开放平台
开放平台:https://open.alipay.com/
沙箱环境:https://openhome.alipay.com/platform/appDaily.htm?tab=info
文档中心:https://openhome.alipay.com/docCenter/docCenter.htm
API中心:https://opendocs.alipay.com/apis
🚬 3、支付能力
当面付帮助商家在线下消费场景中实现快速收款,支持 条码支付 和 扫码支付 两种付款方式。商家可通过以下两种任⼀方式进行收款,提升收银效率,实现资金实时到账。
-
条码支付:买家出示支付宝钱包中的条码、二维码,商家扫描用户条码即可完成 条码支付收款
-
扫码支付:买家通过使用支付宝 扫⼀扫 功能,扫描商家收款二维码即可完成扫码支付 付款。
产品特色
- 用户仅出示 付款码 或 扫⼀扫 即可完成付款,方便快捷。
- 用户手机无网络要求,可离线支付。
- 支付宝会根据交易金额、登录状态等信息判断是否需要用户输入密码,保障安
全。 - 商家收款资金实时到账,无现金流压力。
🚭 条码支付应用场景
使用流程:
使用说明:
1、收银员在商家收银系统操作生成订单,输入收款金额;
2、用户登录支付宝,点击⾸页付钱/收钱,进入付款码界面,出示给商家;
3、收银员通过扫码设备来扫描用户手机上的条码/二维码后,商家收银系统提交
支付;
4、支付后商家收银系统会拿到支付成功或者失败的结果,用户支付宝 App 显示
收单支付引导或成功结果。
🚭 扫码支付应用场景
适用于单件商品单独定价、无⼈值守、自助售货机等商家。用户打开支付宝中的扫⼀扫 功能,扫描商家展示的二维码进行支付。该模式适用于线下实体店支付、面对面支付、自助售货机等场景。
使用流程:
使用说明:
- 1、收银员在商家收银系统操作生成支付宝订单,并生成二维码;
- 2、用户登录支付宝,点击⾸页 扫⼀扫 或点击 付钱 > 扫码付,进入扫码界面;
- 3、用户扫收银员提供的二维码,核对金额并确认支付;
- 4、用户付款支付宝提示成功或失败,商家收银系统会拿到支付成功或者失败的结果。
🚭 App 支付
App 支付适用于商家在 App 应用中集成支付宝支付功能。商家 App 调用支付宝提
供的 SDK,SDK 再调用支付宝 App 内的支付模块。
- 1、如果用户已安装支付宝 App,商家 App 会跳转到支付宝中完成支付,支付完后
跳回到商家 App 内,最后展示支付结果。 - 2、如果用户没有安装支付宝 App,商家 App 内会调起支付宝网页支付收银台,用
户登录支付宝账户,支付完后展示支付结果。 ⽬前支持手机系统有:iOS(苹
果)、Android(安卓)。
应用案例:
⽬前已上线支付案例,商家可进行实际体验:饿了么 App、优酷 App、携程 App。
🚭 手机网站支付
为方便商家在移动端网页应用中集成支付宝支付功能,支付宝提供了手机网站支付
能力。
说明:手机网站支付产品不建议在 App 端使用;如果需要在 App 端中使用支付,
请接入 App 支付产品,接入文档详见App 支付 开发文档。
业务逻辑:
适用于商家在移动端网页应用中集成支付宝支付功能。 商家在网页中调用支付宝提供的网页支付接口调起支付宝客户端内的支付模块,商家网页会跳转到支付宝中完成支付,支付完后跳回到商家网页内,最后展示支付结果。若无法唤起支付宝客户端,则在⼀定的时间后会自动进入网页支付流程。
🚭 电脑网站支付
为方便网页应用商家接入支付宝支付功能,支付宝提供了电脑网站支付能力,商家可通过开放接口快速集成接入支付宝支付功能。电脑网站为即时到账升级而来的能力。
用户在 PC 端访问商家网站进行消费,通过电脑网站支付,可直接跳转到支付宝 PC网站收银台完成付款。 交易资金直接打入商家支付宝账户,实时到账。 用户交易款项即时到账,交易订单三个月内可退款,提供退款、清结算、对账等配套服务。
基本流程:双十一购物流程
🚄 支付宝介入指引
🚬 1、入住平台
-
①首先登录支付宝平台
-
②选入接入类型
-
③填写接入信息
-
如果是第一次,需要注册,具体按照下面要求进行填写
-
-
④注册成功
-
⑤编写对接的应用名称
-
⑥进入控制台
🚬 2、使用沙箱支付
- 沙箱环境是支付宝开放平台为开发者提供的与生产环境完全隔离的联调测试环境, 开发者在沙箱环境中完成的接口调用不会对生产环境中的数据造成任何影响。
- 支付宝提供的沙箱环境(https://open.alipay.com/develop/sandbox/app)
- 沙箱为开放的产品提供有限功能范围的支持,可以覆盖产品的绝大部分核心链路和 对接逻辑,便于开发者快速学习/尝试/开发/调试。
- 沙箱环境会自动完成或忽略一些场景的业务门槛,
- 例如:开发者无需等待产品开 通,即可直接在沙箱环境调用接口,使得开发集成工作可以与业务流程并行,从而 提高项目整体的交付效率。
注意:
- 由于沙箱环境并非100%与生产环境一致,接囗的实际响应逻辑请以生产环境为准,沙箱环境开发调试完成后,仍然需要在生产环境进行测试验收。
- 沙箱环境拥有完全独立的数据体系,沙箱环境下返回的数据(例如用户ID等) 在生产环境中都是不存在的,开发者不可将沙箱环境返回的数据与生产环境中 的数据混淆。
- ⑦申请成功,支付宝已经给你分配好了模拟的商家号,以及一系列所需要用到的AppID
- 由于使用的是沙盒环境,应用程序的私钥和公钥都已经有了默认配置,我们可以使用默认的公私钥
🚬 3、配置秘钥
APPID是自动帮我们创建了,网关也不用管。我们要做的就是设置那个密钥, 这里RSA2需要设置公钥。推荐使用RSA2
安装阿里支付助手AlipayDevelopmentAssistant工具。生成秘钥
🚒 支付宝入门案例
🚬 1、需要描述
点击进入在支付页面点击付款,完成沙箱支付操作
🚬 2、支付请求ApI描述
🚬 3、搭建环境
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!--springBoot依赖包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 支付功能SDK -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.10.124.ALL</version>
</dependency>
<!--springBoot支持jsp-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
🚬 4、编写Application
# 应用名称
spring.application.name=ALiPayDemo1
# 页面的默认前缀
spring.mvc.view.prefix=/
# 响应页面默认的后缀
spring.mvc.view.suffix=.jsp
# 端口号
server.port=8080
# 项目访问路径
server.servlet.context-path=/
🚬 5、编写工具类
utils - AppUtils
public class AppUtils {
// 定义应用的id ,AppId 就是对应的支付宝账号
public static String app_id = "2021000121695303";
// 定义商户的私钥
public static String merchant_private_key ="xxx";
// 定义商户的公钥 PKCS8格式RSA2私钥
public static String align_public_key= "xxx";
// 服务器异步通知页面路径
// 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
public static String notify_url= "";
// 页面跳转同步通知页面路径
// 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
public static String return_url= "";
// 签名方式
public static String sign_type = "RSA2";
// 字符的编码格式
public static String charset = "utf-8";
// 支付宝网关,注意这些使用的是沙箱的支付宝网关,与正常网关的区别是多了dev
public static String gatewayUrl= "https://openapi.alipaydev.com/gateway.do";
// 日志地址
public static String log_path = "F:\\桌面\\";
/**
* 写⽇志,方便测试(看网站需求,也可以改成把记录存入数据库)
* @param sWord 要写入⽇志⾥的文本内容
*/
public static void logResult(String sWord){
// 创建一个文件流
FileWriter fieldWriter = null;
try {
fieldWriter = new FileWriter(log_path + "alipay_log" + System.currentTimeMillis() + ".text");
fieldWriter.write(sWord);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fieldWriter != null) {
try {
fieldWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
utils - BeanUtils
/**
* 提供支付宝相关的一些对象
*/
@Configuration
public class BeanUtils {
// 创建支付宝需要的客户端对象
@Bean
public AlipayClient alipayClient(){
return new DefaultAlipayClient(
AppUtils.gatewayUrl, // 网关
AppUtils.app_id, // appid
AppUtils.merchant_private_key, // 私钥
"json", // 数据格式
AppUtils.charset, // 编码方式
AppUtils.align_public_key, // 公钥
AppUtils.sign_type // 签名类型
);
}
// 创建一个支付宝的请求对象
@Bean
public AlipayTradePagePayRequest alipayTradePagePayRequest() {
return new AlipayTradePagePayRequest();
}
}
🚬 6、Controller 请求处理
/**
* 支付的控制器
*/
@Controller
public class PayController {
@Autowired
private AlipayClient alipayClient;
@Autowired
private AlipayTradePagePayRequest alipayTradePagePayRequest;
/**
* 接收页面传递传递过来的参数
*
* @param WIdOut_trade_no 订单号
* @param WIdSubject 金额
* @param WIdTotal_amount 名称
* @param WIdBody 商品描述
* @param response 表单中的其他参数信息 name值 = 参数名
* @throws Exception
*/
@RequestMapping("/pay")
public void pay(String WIdOut_trade_no, String WIdSubject,
String WIdTotal_amount, String WIdBody, HttpServletResponse response)
throws Exception {
// 1、设置参数
// 设置响应的地址(支付宝返回给商户的响应地址)
alipayTradePagePayRequest.setNotifyUrl(AppUtils.notify_url); // 内网穿透 支付宝像我们的应用发送一个消息,支付成功还是失败的消息
alipayTradePagePayRequest.setReturnUrl(AppUtils.return_url); // 通知页面
// 设置其他参数 (传递给支付宝的数据)
alipayTradePagePayRequest.setBizContent(
"{\"out_trade_no\":\""+ WIdOut_trade_no +"\","
+ "\"total_amount\":\""+ WIdTotal_amount +"\","
+ "\"subject\":\""+ WIdSubject +"\","
+ "\"body\":\""+ WIdBody +"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}"); // 产品的编码
// 2、发送请求 调用 pageExecute 传递 请求 getBody 将结果以字符串的方式返回回来
String result = alipayClient.pageExecute(alipayTradePagePayRequest).getBody();
// 3、将响应结果返回给前端
response.setContentType("text/html;charset=utf-8");
response.getWriter().println(result);
}
/**
* 流程:
*
* 1、封装参数,封装到alipayTradePagePayRequest参数中,
* 2、封装好之后发送请求,调用pageExecute()方法,调用getBody()返回信息
* 3、可以把支付宝返回的信息,通过流的方式发送给页面
*/
}
🚬 7、配置异步通知处理器
异步通知: 其实是双保险机制,如果同步通知后没有跳转到你的网址,可能用户关了,可能网速慢,即无法触发你更新订单状态为已支付的controller,这时候异步通知就有作用了,不过你要判断一下,如果订单已经变为已支付,则不必再更新一次了,只返回给支付宝success即可,否则他会一直异步通知你
异步通知参数说明文档: https://opendocs.alipay.com/open/203/105286
/**
* 异步通知控制器
*/
@Controller
public class NotifyController {
/**
* 接收支付宝返回的异步通知信息
*
* @param request
* @param response
*/
@RequestMapping("/getNotify")
public void getNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 获取⽀付宝POST过来反馈信息
Map<String, String> params = new HashMap<>();
Map<String, String[]> parameterMap = request.getParameterMap(); // 获取参数
/**
keySet()和entrySet(),是Map集合中的两种取值方法。
与get(Object key)相比,
get(Object key)只能返回到指定键所映射的值,不能一次全部取出
keySet()和entrySet()可以。
Map集合中没有迭代器,Map集合取出键值的原理:将map集合转成set集合,再通过迭代器取出。
*/
Iterator<String> iterator = parameterMap.keySet().iterator();// iterator 迭代器
while (iterator.hasNext()) {
String name = iterator.next();
String[] values = parameterMap.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
// 拼接 values信息 如果是最后一个值, 不拼接 ,号
valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
}
// 乱码解决,这段代码在出现乱码时使⽤
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
// 调用支付宝的SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(
params, // 参数
AppUtils.align_public_key, // 公钥
AppUtils.charset, // 编码方式
AppUtils.sign_type); // 签名类型
/**
* 主要用途:
* 判断信息有没有被篡改
* 1、需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,
* 2、判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额)
* 3、校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作⽅(有的时候,⼀个商户可能有多个seller_id/seller_email)
* 4、验证app_id是否为该商户本身。
*/
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
if (signVerified) {//验证成功
String out_trade_no = request.getParameter("out_trade_no"); // 商户订单号
String trade_no = request.getParameter("trade_no"); // ⽀付宝交易号
String trade_status = request.getParameter("trade_status"); // 交易状态
/**
* 支付宝Api 交易状态说明
* 枚举名称 枚举说明
* WAIT_BUYER_PAY 创建交易,等待买家付款
* TRADE_CLOSED 未付款交易超时关闭
* TRADE_SUCCESS 交易支付成功
* TRADE_FINISHED 交易结束,不可退款
*/
if (trade_status.equals("TRADE_FINISHED")) {
System.out.println("交易结束,不可退款");
out.println("finished");
} else if (trade_status.equals("TRADE_SUCCESS")) {
System.out.println("交易支付成功");
out.println("success");
}
} else {//验证失败
System.out.println("验证失败");
out.println("fail");
}
}
}
🚬 8、内网穿透配置
由于我们的项目是内网,支付宝通知不能通知支付信息,因此我们需要是用内网穿透技术将ip映射为一个支付宝可以通知到的外网地址。
自行配置:
命令:natapp -authtoken=404390a06dca8d75
添加异步通知地址
# AppUtils 异步通知的地址
public class AppUtils {
public static String notify_url= "http://cwgmzt.natappfree.cc/getNotify";
}
🚬 9、添加同步通知处理类
同步返回页面控制器
# AppUtils 同步通知的地址
public class AppUtils {
public static String notify_url= "http://cwgmzt.natappfree.cc/getReturn";
}
/**
* 同步返回页面控制器
*/
@Controller
public class ReturnController {
/**
* 接收支付宝返回的同步通知信息
*
* @param request
* @param response
*/
@RequestMapping("/getReturn")
public void getReturn(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 获取⽀付宝POST过来反馈信息
Map<String, String> params = new HashMap<>();
Map<String, String[]> parameterMap = request.getParameterMap(); // 获取参数
/**
keySet()和entrySet(),是Map集合中的两种取值方法。
与get(Object key)相比,
get(Object key)只能返回到指定键所映射的值,不能一次全部取出
keySet()和entrySet()可以。
Map集合中没有迭代器,Map集合取出键值的原理:将map集合转成set集合,再通过迭代器取出。
*/
Iterator<String> iterator = parameterMap.keySet().iterator();// iterator 迭代器
while (iterator.hasNext()) {
String name = iterator.next();
String[] values = parameterMap.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
// 拼接 values信息 如果是最后一个值, 不拼接 ,号
valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
}
// 乱码解决,这段代码在出现乱码时使⽤
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
// 调用支付宝的SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(
params, // 参数
AppUtils.align_public_key, // 公钥
AppUtils.charset, // 编码方式
AppUtils.sign_type); // 签名类型
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
if (signVerified) {//验证成功
String out_trade_no = request.getParameter("out_trade_no"); // 商户订单号
String trade_no = request.getParameter("trade_no"); // ⽀付宝交易号
String trade_amount = request.getParameter("trade_amount"); // 交易状态
// 金额
out.println("out_trade_no" + out_trade_no + "trade_no" + trade_no + "trade_amount" + trade_amount);// 返回到浏览器
} else {//验证失败
System.out.println("验证失败");
out.println("fail");
}
}
}
🚬 10、编写支付前台页面
🚬 11、支付测试
编写登录控制器方法
# PayController
/**
* 跳转到index.html页面
*
* @return
*/
@RequestMapping("/index")
public String index() {
return "index";
}
成功
🚤 支付宝项目
🚬 1、项目构建
🚬 2、搭建持久层
🚭 1、数据库表结构
🚭 2、创建实体
🚭 3、统一返回对象(工具类)
存放常量的接口
/**
* 存放常量的接口
*/
public interface SysConstant {
/**
* 默认的成功状态
*/
int STATUS_SUCCESS = 200;
/**
* 默认的成功消息
*/
String SUCCESS_MESSAGE = "success";
/**
* 默认的失败消息
*/
int STATUS_ERROR = 500;
/**
* 默认的失败消息
*/
String ERROR_MESSAGE = "error";
/**
* 订单的状态 - 代付款
*/
String WAIT_BUYER_PAY = "WAIT_BUYER_PAY";
/**
* 订单的状态 - 交易成功
*/
String TRADE_SUCCESS = "TRADE_SUCCESS";
/**
* 订单的状态 - 订单取消
*/
String CANCEL_SUCCESS = "CANCEL_SUCCESS";
/**
* 订单的状态 - 退款成功
*/
String REFUND_SUCCESS = "REFUND_SUCCESS";
/**
* 订单的状态 - 退款失败
*/
String REFUND_FAIL = "REFUND_FAIL";
/**
* 订单的状态 - 订单已关闭
*/
String TRADE_CLOSED = "TRADE_CLOSED";
/**
* 订单的状态 - 支付的数据格式
*/
String FORMAT = "json";
}
统一的返回的对象
/**
* 统一的返回的对象
*/
@Data
public class BaseResult {
// 返回的状态码
private int status;
// 返回消息
private String message;
// 返回数据
private Object result;
// 返回时间
private long timestamp = System.currentTimeMillis();
// 返回创建的对象
private static BaseResult createResult(int status, String message, Object result) {
BaseResult baseResult = new BaseResult();
baseResult.setStatus(status);
baseResult.setMessage(message);
baseResult.setResult(result);
return baseResult;
}
// 返回成功的方法,带数据
public static BaseResult success(Object result) {
return BaseResult.createResult(SysConstant.STATUS_SUCCESS, SysConstant.SUCCESS_MESSAGE,result);
}
// 返回成功的方法,不带数据
public static BaseResult success() {
return BaseResult.createResult(SysConstant.STATUS_SUCCESS, SysConstant.SUCCESS_MESSAGE,null);
}
// 返回失败的方法,带数据
public static BaseResult error(Object result) {
return BaseResult.createResult(SysConstant.STATUS_ERROR, SysConstant.ERROR_MESSAGE,result);
}
// 返回失败的方法,不带数据
public static BaseResult error() {
return BaseResult.createResult(SysConstant.STATUS_ERROR, SysConstant.ERROR_MESSAGE,null);
}
}
🚭 4、整合mybatis-plus
MyBatisPlus 配置类
/**
* MyBatisPlus 配置类
*/
@MapperScan("com.gh.mapper")
@EnableTransactionManagement // 开启事务管理器
@Configuration
public class MyBatisConfig {
}
🚭 5、测试结果
查询订单表中的数据
/**
* 订单控制器
*/
@Controller
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("/list")
@ResponseBody
public BaseResult list() {
List<Order> list = orderService.list();
return BaseResult.success(list);
}
}
成功
🚬 3、引入支付宝
🚭 1、引入依赖
<!-- 支付功能SDK -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.10.124.ALL</version>
</dependency>
🚭 2、添加配置
#支付宝的配置信息
alipay:
app_id: 2021000121695303
merchant_private_key: # 私钥
alipay_public_key: # 公钥
notify_url: http://dnut8g.natappfree.cc/alipay/trade/notify # 异步通知地址
return_url: http://localhost:8080/success # 同步返回地址
sign_type: RSA2 # 签名的方式
charset: utf-8 # 编码方式
gatewayUrl: https://openapi.alipaydev.com/gateway.do # 网关
🚭 3、编写配置类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Component
@ToString
@ConfigurationProperties(prefix = "alipay") // 读取配置文件 给一个前缀
public class AlipayConfig {
//应用的id app_id
@Value("${alipay.app_id}")
public String app_id;
//商户的私钥 merchant_private_key
@Value("${alipay.merchant_private_key}")
public String merchant_private_key;
//支付宝的公钥
@Value("${alipay.alipay_public_key}")
public String alipay_public_key;
//异步通知的地址
@Value("${alipay.notify_url}")
public String notify_url;
//同步跳转的页面
@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;
//定义一个方法返回AlipayConfig对象
@Bean
public AlipayConfig getAlipayConfig(){
AlipayConfig alipayConfig = new AlipayConfig();
alipayConfig.setApp_id(app_id);
alipayConfig.setApp_id(merchant_private_key);
alipayConfig.setApp_id(alipay_public_key);
alipayConfig.setApp_id(notify_url);
alipayConfig.setApp_id(return_url);
alipayConfig.setApp_id(sign_type);
alipayConfig.setApp_id(charset);
alipayConfig.setApp_id(gatewayUrl);
return alipayConfig;
}
}
🚭 4、编写工具类
/**
* 支付的工具类
*/
public class StringUtils {
public static void main(String[] args) {
}
/**
* 生成订单
*/
public static String createOderNum() {
String date = new SimpleDateFormat("yyyyMMdd").format(new Date());
String seconds = new SimpleDateFormat("HHmmss").format(new Date());
System.out.println(date);
System.out.println(seconds);
String result = date + "000010000" + getTwo() + "00" + seconds + getTwo();
return result;
}
/**
* 生成一个随机的两位数
*/
public static String getTwo() {
Random random = new Random();
String result = random.nextInt(100) + "";
if (result.length() == 1) {
result = "0" + result; // 如果随机的是一位数,则加上 0
}
return result;
}
}
🚬 4、支付功能
支付流程
统一收单下单并支付页面接口
参考链接 https://opendocs.alipay.com/open/028r8t?ref=api&scene=22
🚭 1、OrderService接口
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order>
implements OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 创建订单
*/
@Override
public Order createOrder() {
// 1、生成订单编号
Order order = new Order();
order.setSubject("测试数据");
order.setOrder_no(StringUtils.createOderNum());
order.setTotal_amount(0.1D);
order.setStatus(SysConstant.WAIT_BUYER_PAY);
order.setCreate_time(new Timestamp(new Date().getTime()));
order.setUpdate_time(new Timestamp(new Date().getTime()));
// 写入数据
orderMapper.insert(order);
return order;
}
}
🚭 2、支付ApiPayService
- 1、创建订单 --> 根据订单支付
- 2、获取参数 (公共参数 和 请求参数)
- 3、获取客户端对象
- 4、创建支付 请求对象
- 5、组装biz_content请求参数的集合 JSON类型
- 参数要和 官网中的请求参数 一致
- 6、重点: 调用远程的支付宝支付接口
public interface AlipayService {
/**
* 创建支付
*/
String createTrade();
}
/**
* 阿里支付Service
*/
@Service
@Slf4j
public class AlipayServiceImpl implements AlipayService {
@Autowired
private OrderService orderService;
@Autowired
private AlipayConfig alipayConfig;
/**
* 创建支付
*/
@Override
public String createTrade() {
// 1、创建订单 --> 根据订单支付
log.info("创建订单");
Order order = orderService.createOrder();
// 2、获取参数 (公共参数 和 请求参数)
// 支付宝的appid
String app_id = alipayConfig.app_id;
// 获取商户私钥
String merchant_private_key = alipayConfig.merchant_private_key;
// 获取商户公钥
String alipay_public_key = alipayConfig.alipay_public_key;
// 支付宝异步通知地址
String notify_url = alipayConfig.notify_url;
// 同步通知地址
String return_url = alipayConfig.return_url;
// 编码
String charset = alipayConfig.charset;
// 签名方式
String sign_type = alipayConfig.sign_type;
// 网关
String gatewayUrl = alipayConfig.gatewayUrl;
// 3、获取客户端对象
AlipayClient alipayClient = new DefaultAlipayClient(
gatewayUrl,
app_id,
merchant_private_key,
SysConstant.FORMAT,// 数据格式类型json
charset,
alipay_public_key,
sign_type
);
// 4、创建支付 请求对象
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
// 设置请求的异步通知路径
request.setNotifyUrl(notify_url);
// 设置请求的同步通知路径
request.setReturnUrl(return_url);
// 5、组装biz_content请求参数的集合 JSON类型
// 参数要和 官网中的请求参数 一致
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no",order.getOrder_no()); // 商品的订单号
bizContent.put("total_amount",order.getTotal_amount()); // 订单金额
bizContent.put("subject",order.getSubject()); // 订单标题
bizContent.put("product_code","FAST_INSTANT_TRADE_PAY"); // 商品的产品码 固定值
// 存放到request 请求中
request.setBizContent(String.valueOf(bizContent));
// 6、重点: 调用远程的支付宝支付接口
AlipayTradePagePayResponse response = null;
try {
response = alipayClient.pageExecute(request);
// 判断是否支付成功
if (response.isSuccess()) {
log.info("支付成功");
}else {
log.info("支付失败");
}
} catch (AlipayApiException e) {
e.printStackTrace();
}
return response.getBody();
}
}
🚭 3、ApiPayController控制器
/**
* 支付控制器
*/
@Controller
@RequestMapping("alipay")
public class AlipayController {
@Autowired
private AlipayService alipayService;
/**
* 支付方法
*/
@RequestMapping("trade")
public void alipay(HttpServletResponse response) throws IOException {
// 1、创建支付
String trade = alipayService.createTrade();
response.setContentType("text/html;charset=utf-8");
response.getWriter().write(trade);
}
}
🚬 5、搭建前端
🚭 1、整合Lay-ui前台
复制静态资源到resources/public下
页面台转控制器
/**
* 页面跳转的控制器
*/
@Controller
public class IndexController {
//跳转到index.html
@RequestMapping("/index")
public String index(){
return "index";
}
//跳转到main.html
@RequestMapping("/main")
public String main(){
return "main";
}
//跳转到order的列表页面
@RequestMapping("/orderList")
public String orderList(){
return "orderList";
}
//支付成功后跳转到sucess
@RequestMapping("/success")
public String success(){
return "success";
}
}
🚬 6、支付宝功能前台
main.html
🚬 7、支付返回成功页面
success.html
🚬 8、异步通知
开启内网穿透 复制地址到配置文件中
🚬 9、异步返回验签
参考链接 https://opendocs.alipay.com/support/01rawm
AlipayController 异步通知
/**
* 异步通知
*/
@PostMapping("/trade/notify")
public String tradeNotify(@RequestParam Map< String,String> params) throws AlipayApiException {
System.out.println(params);
// 验签操作
//1.验证证书上的公钥是否一致
boolean signVerified = alipayService.aliSignature(params); // 验签的方法
if(!signVerified){
log.info("验证失败,验证签名失败");
return "fail";
}
//2.商户发送的订单号是否与返回的一致
String out_trade_no = params.get("out_trade_no");
Order order = orderService.getByTradeNo(out_trade_no);
if(order == null){
log.info("验证失败,订单不存在");
return "fail";
}
//3.验证金额与发送的是否一致
String total_amount = params.get("total_amount");
double parseDouble = Double.parseDouble(total_amount);
Double totalAmount = order.getTotal_amount();
if(parseDouble != totalAmount){
log.info("验证失败,金额不一致");
return "fail";
}
//4.验证支付宝返回的状态是否一致
String trade_status = params.get("trade_status");
if(!SysConstant.TRADE_SUCCESS.equals(trade_status)){
log.info("验证失败,交易状态不一致");
return "fail";
}
//5.修改订单的状态
orderService.updateOrderStatus(order,SysConstant.TRADE_SUCCESS);
return "success";
}
AlipayServiceImpl 验证签名的方法
/**
* 验证签名的方法
*/
@Override
public boolean aliSignature(Map<String, String> params) throws AlipayApiException {
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayConfig.alipay_public_key, alipayConfig.charset, alipayConfig.sign_type);
if(!signVerified){
return false;
}
return true;
}
OrderServiceImpl 根据订单编号获取订单
OrderServiceImpl 更新订单信息
/**
* 根据订单编号获取订单
*/
@Override
public Order getByTradeNo(String out_trade_no) {
//定义查询条件
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",out_trade_no);
Order order = orderMapper.selectOne(queryWrapper);
return order;
}
/**
* 更新订单信息
*/
@Override
public void updateOrderStatus(Order order, String tradeSuccess) {
order.setStatus(tradeSuccess);
order.setUpdate_time(new Timestamp(new Date().getTime()));
orderMapper.updateById(order);
}
🚬 10、查询订单
🚭 1、修改配置nav.json
🚭 2、编写orderList.html
🚭 3、编写跳转页
@Controller
public class IndexController {
//跳转到order的列表页面
@GetMapping("/orderList")
public String orderList(){
return "orderList";
}
}
🚭 4、编写OrderController控制器方法
/**
* 订单控制器
*/
@Controller
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 获取订单列表 --> 数据格式对应 userList格式
* @return
*/
@GetMapping("/list")
@ResponseBody
public Map<String,Object> list(){
List<Order> list = orderService.list();
Map<String,Object> map = new HashMap<>();
map.put("code",0);
map.put("msg","");
map.put("count",list.size());
map.put("data",list);
return map;
}
}
🚭 5、订单展示效果
🚬 11、取消订单
链接地址 https://opendocs.alipay.com/open/028wob?ref=api
🚭 1、AliPayController
/**
* 关闭订单
*/
@ResponseBody
@PostMapping("/closeTrade/{orderNo}")
public BaseResult closeTrade(@PathVariable String orderNo) throws AlipayApiException {
alipayService.closeTrade(orderNo);
return BaseResult.success("取消订单成功");
}
🚭 2、AliPayService
两种情况
- 1、登录到支付宝、支付宝后台会生成订单信息
- 2、没有登录:在义务系统生成订单消息
/**
* 关闭订单
*/
void closeTrade(String orderNo) throws AlipayApiException;
/**
* 关闭订单
*/
@Override
public void closeTrade(String orderNo) throws AlipayApiException {
//1.获取alipay的客户端对象
AlipayClient alipayClient = getClient();
//2.创建一个关闭请求对象
AlipayTradeCloseRequest alipayTradeCloseRequest = new AlipayTradeCloseRequest();
//3.设置参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no",orderNo);
alipayTradeCloseRequest.setBizContent(bizContent.toString());
//4.发送请求关闭订单
AlipayTradeCloseResponse response = alipayClient.execute(alipayTradeCloseRequest);
if(response.isSuccess()){
log.info("关闭订单成功");
}else{
log.info("关闭订单失败");
}
//5.更新订单
orderService.updateOrderStatus(orderService.getByTradeNo(orderNo),SysConstant.CANCEL_SUCCESS);
}
//获取客户端对象
public AlipayClient getClient(){
//2.获取参数 (公共参数和请求参数)
//获取支付宝的appid
String app_id = alipayConfig.app_id;
//获取商户的私钥
String merchant_private_key = alipayConfig.merchant_private_key;
//获取阿里的公钥
String alipay_public_key = alipayConfig.alipay_public_key;
//支付宝异步通知地址
String notify_url = alipayConfig.notify_url;
//同步通知地址
String return_url = alipayConfig.return_url;
//编码
String charset = alipayConfig.charset;
//签名方式
String sign_type = alipayConfig.sign_type;
//网关
String gatewayUrl = alipayConfig.gatewayUrl;
//3.获取客户端对象
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,app_id,merchant_private_key, SysConstant.FORMAT,charset,alipay_public_key,sign_type);
return alipayClient;
}
🚭 3、取消订单前台
OrderList
🚭 4、测试
🚬 12、退款功能
🚭 1、退款流程
交易退款接口 https://opendocs.alipay.com/open/028sm9?ref=api
交易退款查询接口 https://opendocs.alipay.com/open/028sma?ref=api
冲退完成通知 https://opendocs.alipay.com/open/029yy3?ref=api
🚭 2、退款业务层
退款需要通过订单ID进⾏退款,因此传递订单编号
/**
* 退款
*/
@ResponseBody
@PostMapping("/refund/{orderNo}")
public BaseResult refund(@PathVariable String orderNo) throws AlipayApiException {
alipayService.refund(orderNo);
return BaseResult.success(SysConstant.REFUD_SUCCESS);
}
🚭 3、退款Service层
/**
* 退款
* @param orderNo
* @throws AlipayApiException
*/
void refund(String orderNo) throws AlipayApiException;
/**
*
* @param orderNo
* @throws AlipayApiException
*/
@Override
public void refund(String orderNo) throws AlipayApiException {
//1.获取alipay的客户端对象
AlipayClient alipayClient = getClient();
//2.创建退款的request对象
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
Order order = orderService.getByTradeNo(orderNo);
//3.组装业务参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no",orderNo);
bizContent.put("refund_amount",order.getTotalAmount());
request.setBizContent(bizContent.toString());
//4.请求支付宝退款
AlipayTradeRefundResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("退款成功,金额原路返回" + response.getBody());
}else {
log.info("退款失败" + response.getCode());
}
//5.更新订单状态
orderService.updateOrderStatus(order,SysConstant.REFUD_SUCCESS);
}
🚭 4、退款到前台
🚭 5、退款查询
Controller
/**
* 退款查询
*/
@ResponseBody
@GetMapping("/refundQuery/{orderNo}")
public BaseResult queryRefund(@PathVariable String orderNo) throws AlipayApiException {
String result = alipayService.queryRefund(orderNo);
return BaseResult.success(result);
}
Service
/**
* 退款查询
* @param orderNo
* @return
*/
String queryRefund(String orderNo) throws AlipayApiException;
/**
* 退款查询
* @param orderNo
* @return
*/
@Override
public String queryRefund(String orderNo) throws AlipayApiException {
//1.获取alipay的客户端对象
AlipayClient alipayClient = getClient();
//2.创建退款的request对象
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
//3.组装业务参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no",orderNo);
bizContent.put("out_request_no",orderNo);
request.setBizContent(bizContent.toString());
//4.请求查询退款信息
AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
log.info("查询成功" + response.getBody());
return response.getBody();
}else {
log.info("查询失败" + response.getCode());
return response.getCode();
}
}
测试 http://localhost:8081/alipay/refundQuery/20221211000010000500021182263 (订单链接)
🚗 沙箱环境切换到正式环境
🚬 1、更改app_id
将代码中沙箱应用的APPID修改为正式环境创建的应用appid, 详情参考
🚬 2、修改网关支付宝网关
沙箱环境网关切换成正式环境的支付宝网关。
🚬 3、修改秘钥
注:即使是相同的密钥,沙箱环境和正式环境的支付宝公钥也是不一样的,需要修改。
🚬 4、请求参数
请求参数中绑定的沙箱收款账户或者沙箱付款账户,请修改为正式环境的账户。
🚬 5、添加权限
由于相较沙箱环境测试,正式环境应用必须添加生效的权限, 详见如何添加应用功能
🚏 微信支付
微信⽀付的参考平台:https://pay.weixin.qq.com/
api秘钥和证书申请 : https://kf.qq.com/faq/180830E36vyQ180830AZFZvu.html
接入准备:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_1.shtml
提供资料 : https://kf.qq.com/faq/180910aYF77n18091073a6Jj.html
- (个体⼯商户)接⼊微信⽀付需要哪些资料?https://kf.qq.com/faq/180910eUfeyu180910BFZNJN.html
- (企业)接⼊微信⽀付需要哪些资料?https://kf.qq.com/faq/180910aYF77n18091073a6Jj.html
- (政府机关)如何提交接入申请资料? https://kf.qq.com/faq/190313vyY3Mf190313bmUnUB.html
(社会组织)如何提交接入申请资料?https://kf.qq.com/faq/180910yInUBb180910NBriyi.html - (事业单位)如何提交接入申请资料?https://kf.qq.com/faq/220704y2MN7R2207047bm2iU.html
🚀 项目的结构构建
🚬 实体类
- 支付宝未用到的 补充信息
@Data
@TableName("t_order")
public class Order {
//二维码的url
private String codeUrl;
}
🚬 存放常量的接口SysConstant
- 支付宝未用到的 补充信息
/**
* 存放常量的接口
*/
public interface SysConstant {
//Native下单
String NATIVE_PAY = "/v3/pay/transactions/native";
//订单查询
String ORDER_QUERY_BY_NO = "/v3/pay/transactions/out-trade-no/%s";
//关闭订单
String CLOSE_ORDER_BY_NO = "/v3/pay/transactions/out-trade-no/%s/close";
//申请退款
String DOMESTIC_REFUNDS = "/v3/refund/domestic/refunds";
//支付通知
String NATIVE_NOTIFY = "/wxpay/notify";
//退款通知
String REFUND_NOTIFY = "/wxpay/refundsNotify";
}
🚄 微信APIV3的介绍
为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付API v3。
相较于之前的微信支付API,主要区别是:
- 遵循统一的REST的设计风格
- 使用JSON作为数据交互的格式,不再使用XML
- 使用基于非对称密钥的SHA256-RSA的数字签名算法,不再使用MD5或HMACSHA256
- 不再要求携带HTTPS客户端证书(仅需携带证书序列号)
- 使用AES-256-GCM,对回调中的关键信息进行加密保护
APIV3 官方SDK
🚬 APIV3证书和密钥使用流程
🚒 引入微信支付
🚬 maven
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.3.0</version>
</dependency>
<!-- 作用: 让 yml中配置内容 与WxPayConfig 链接-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
🚬 yml
wxpay:
# 微信支付相关参数
# 商户号
mch-id: 1497984412
# 商户API证书序列号
mch-serial-no: 457781397EF26D03680D05C7C5C1164003FEDD8B
# 商户私钥文件
private-key-path: D://apiclient_key.pem
# APIv3密钥
api-v3-key: qianfengjiaoyuceshihao1234567891
# APPID
appid: wx632c8f211f8122c6
# 微信服务器地址
domain: https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
notify-domain: http://z2r2zh.natappfree.cc
# APIv2密钥
partnerKey: qianfengjiaoyuceshihao1234567891
🚬 微信的配置类(重)
1、获取商户的私钥
2、获取签名的验证器: 签名验证器会帮我们进行验签操作
3、获取httpClient对象: 建立远程链接的基础,我们通过SDK创建该对象 带验证的方式
4、获取无需验证的相应签名的httpClient
/**
* 微信的配置类
*/
@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "wxpay")
public class WxPayConfig {
//商户号
@Value("${wxpay.mch-id}")
public String mchId;
//商户API证书序列号
@Value("${wxpay.mch-serial-no}")
public String mchSerialNo;
//商户的私钥文件
@Value("${wxpay.private-key-path}")
public String privateKeyPath;
//APIV3秘钥
@Value("${wxpay.api-v3-key}")
public String apiV3Key;
//APPID
@Value("${wxpay.appid}")
public String appid;
//微信的服务器地址
@Value("${wxpay.domain}")
public String domain;
//接收结果的通知地址
@Value("${wxpay.notify-domain}")
public String notifyDomain;
/**
* 获取商户的私钥
*/
private PrivateKey getPrivateKey(String filename) {
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename)); // filename 私钥的地址
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return null;
}
/**
* 获取签名的验证器
* 签名验证器会帮我们进行验签操作
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier() {
//获取商户的私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//获取私钥签名的对象 (签名) 序列号 和 私钥
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象 (验签) 商户号 签名
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
//使用定时更新签名验证器的方式, 就不需要传入证书了
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials, // 验签对象
apiV3Key.getBytes(StandardCharsets.UTF_8)); // apiV3的秘钥转成2进制
return verifier;
}
/**
* 获取httpClient对象: 建立远程链接的基础,我们通过SDK创建该对象
* 带验证的方式
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) {
//获取商户的私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//用来构建HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() // 创建
.withMerchant(mchId, mchSerialNo, privateKey) // 设置: 商户号、序列号、私钥
.withValidator(new WechatPay2Validator(verifier)); // 验签
//使用builder对象构建httpClient
return builder.build();
}
/**
* 获取无需验证的相应签名的httpClient
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPaySignClient(ScheduledUpdateCertificatesVerifier verifier) {
//获取私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator((response) -> true); // 响应无需签名的方式
//创建对象并返回
return builder.build();
}
}
🚤 Native支付API及流程
生成订单 --> 发送预支付 code_url --> 展示给用户 -> 用户扫码支付
商户Native支付下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。
参考链接 :https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml
🚬 业务流程时序图
🚤 Native支付API及流程
生成订单 --> 发送预支付 code_url --> 展示给用户 -> 用户扫码支付
商户Native支付下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。
🚬 业务流程时序图
🚬 Native下单API(重点)
🚭 下单控制器
/**
* 微信的支付控制器
*/
@RequestMapping("/wxpay")
@Controller
@Slf4j
public class WxPayController {
@Resource
private WxPayService wxPayService;
/**
* native下单
*/
@ResponseBody
@PostMapping("/nativePay")
public BaseResult nativePay() throws IOException {
//发送service请求,获取一个code_url
Map<String,Object> map = wxPayService.nativePay();
return BaseResult.success(map);
}
}
🚭 下单WxPayServiceImpl服务
- 1、创建订单
- 2、调用下单API
- 3、完成签名
- 4、获取相应结果
- 5、更新订单中的code_url信息
/**
* native支付
* @return
*/
Map<String, Object> nativePay() throws IOException;
/**
* 微信支付的服务实现
*/
@Service
@Transactional
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
private CloseableHttpClient wxPayClient;
@Resource
private OrderService orderService;
@Resource
private WxPayConfig wxPayConfig;
/**
* 本地支付的方法
* @return
*/
@Override
public Map<String, Object> nativePay() throws IOException {
//1.创建订单
log.info("创建订单开始");
Order wxOrder = orderService.createWxOrder();
//2.调用下单API
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(SysConstant.NATIVE_PAY)); // 拼接地址 微信的前缀加 Native下单
Map<String,Object> paramMap = new HashMap<>();
paramMap.put("appid",wxPayConfig.getAppid()); // appid
paramMap.put("mchid",wxPayConfig.getMchId()); // 商户号
paramMap.put("description",wxOrder.getSubject()); // 描述
paramMap.put("out_trade_no",wxOrder.getOrderNo()); // 订单编号
paramMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(SysConstant.NATIVE_NOTIFY)); // 支付通知url
//存放金额的参数
Map<String,Object> amountMap = new HashMap<>();
amountMap.put("total",wxOrder.getTotalAmount()); // 总金额
amountMap.put("currency","CNY"); // 金币的种类CNY 人民币
paramMap.put("amount",amountMap);
//参数转成json格式
Gson gson = new Gson();
String requestJson = gson.toJson(paramMap);
//将json转成StringEntity对象
StringEntity entity = new StringEntity(requestJson,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept","application/json");
//3.完成签名
CloseableHttpResponse response = wxPayClient.execute(httpPost);
//4.获取相应结果
String bodyString = EntityUtils.toString(response.getEntity()); //请求后得到的响应体
int statusCode = response.getStatusLine().getStatusCode(); // 获取相应码
//如果处理成功(带响应体的)
if(statusCode == 200){
log.info("请求成功 : "+bodyString);
}else if(statusCode == 204){ //处理成功但是不带 body
log.info("处理成功 204");
}else{
log.info("native 下单失败 响应码为 "+ statusCode + " 返回的结果 "+ bodyString);
}
// 转成二维码超链接
//5.更新订单中的code_url信息
//把body中的信息转成map ,直接返回
Map<String , String> resultMap = gson.fromJson(bodyString,HashMap.class);
String code_url = resultMap.get("code_url"); // 获取二维码信息
// 二维码 和 订单绑定
Map<String , Object> map = new HashMap<>();
map.put("codeUrl",code_url);
map.put("orderNo",wxOrder.getOrderNo());
//@TODO
//更新订单中的codeurl信息
orderService.updateOrderCodeUrl(wxOrder,code_url);
return map;
}
}
🚭 更新订单的codeUrl --> OrderServiceImpl
Order createWxOrder();
void updateOrderCodeUrl(Order wxOrder, String code_url);
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper ,Order> implements OrderService {
@Resource
private OrderMapper orderMapper;
/**
* 生成微信支付订单
*/
@Override
public Order createWxOrder() {
//1.生成订单编号
Order order = new Order();
order.setOrderNo(StringUtils.createOrderNum());
order.setSubject("微信测试数据");
order.setStatus(SysConstant.WAIT_BUYER_PAY);
order.setTotalAmount(1);
order.setPayType("WX");
order.setCodeUrl("");
order.setCreateTime(new Timestamp(new Date().getTime()));
order.setUpdateTime(new Timestamp(new Date().getTime()));
//调用mapper写入数据
orderMapper.insert(order);
return order;
}
/**
* 根据订单号来修改codeurl
*/
@Override
public void updateOrderCodeUrl(Order wxOrder, String code_url) {
wxOrder.setCodeUrl(code_url);
wxOrder.setUpdateTime(new Timestamp(new Date().getTime()));
orderMapper.updateById(wxOrder);
}
}
<!--将参数信息装成json-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
//参数转成json格式
Gson gson = new Gson();
String requestJson = gson.toJson(paramMap);
//将json转成StringEntity对象
StringEntity entity = new StringEntity(requestJson,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept","application/json");
🚭 下单前台实现 --> 生成二维码
思路描述:前台点击⽀付–>发送ajax请求到后台(创建订单,设置codeUrl)—>返回codeUrl–>回调函数中生成二维码—> 创建⼀个定时查询后台订单状的方法(每5秒发送⼀次请求,目的是微信支付成功后,后台会更新订单的状态)—>前台定时器不断查询,得到订单状态后可以关闭二维码。
1、引入
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://heerey525.github.io/layui-v2.4.3/layui/layui.js"></script>
<script src="http://static.runoob.com/assets/qrcode/qrcode.min.js"></script>
<script src="./layui/layui.all.js"></script>
2、
<script type="text/javascript">
// 初始化layUi组件
layui.use(['form', 'layer', 'table', 'jquery', 'qrcode'], function () {
var form = layui.form,
layer = parent.layer === undefined ? layui.layer : top.layer,
$ = layui.jquery,
qrcode = layui.laytpl,
table = layui.table;
var orderNo;
var codeUrl;
var interval;
// 微信支付的按钮,发出函数 80行
$("#payEvent").click(function () { /* 给 按钮绑定事件 */
$.ajax({ // 发送post类型的ajax请求 得到nativePay 得到code_url
url: 'wxpay/nativePay',
type: 'POST',
dataType: 'json',
success: function (data) {
orderNo = data.result.orderNo; // 获取订单编号
codeUrl = data.result.codeUrl; // 获取codeUrl
// 打开窗口生成二维码
layer.open({
//所以type设置为1(0:信息框,默认;1:页面层;2:iframe层;3:加载层;4:tips层)。
type: 0,
title: '微信付款码',
//shadeClose: true,
fixed: false, // 可以移动
//resize: true,
shade: false, // 去除背景
// 弹出的内容
content: $('#test').qrcode({ // 一下内容生成二维码
render: "canvas", //table 渲染方式 画布 或者 table
width: 150,
height: 150,
foreground: "#000", // 前景
background: "#fff", // 背景
shade: 0, // 阴影
text: codeUrl // 扫描二维码得到的内容
})
}),
//设置一个周期定时执行的任务
interval = setInterval(function () {
//定时查询订单的状态
$.post("wxpay/queryOrderStatus/" + orderNo, function (data) {
//判断订单状态是否成功
if (data.result.status == "TRADE_SUCCESS") {
//清除定时周期任务
clearInterval(interval);
//关闭open的二维码
$(".layui-layer-content").hide().next().hide().next().hide().next().hide();
layer.msg("交易成功")
}
})
}, 5000)
}
})
})
});
</script>
3、按钮
<button type="button" id="payEvent"
class="layui-btn layui-btn-normal layui-icon layui-icon-search layui-btn-radius layui-btn-sm"
id="xwpay_id">微信支付
</button>
4、编写二维码展示
<!--编写div 用于生成二维码展示-->
<div id="test"></div>
🚭 异步通知接收
1、请求解析工具类:添加工具类HttpUtils 处理微信发送到应用请求
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
public class HttpUtils {
/**
* 将通知参数转化为字符串
*/
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2、验签工具类
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.nio.charset.StandardCharsets;
/**
* 对微信请求进行验签
* 参考微信SDK验签器进行修改,里面验证的是响应,我们要校验的是请求
* com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator
*/
public class WechatPay2ValidatorForRequest{
protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
public static final String REQUEST_ID = "Request-ID";
public static final String WECHAT_PAY_SERIAL = "Wechatpay-Serial";
public static final String WECHAT_PAY_SIGNATURE = "Wechatpay-Signature";
public static final String WECHAT_PAY_TIMESTAMP = "Wechatpay-Timestamp";
public static final String WECHAT_PAY_NONCE = "Wechatpay-Nonce";
/**
* 应答超时时间,单位为分钟
*/
protected static final long RESPONSE_EXPIRED_MINUTES = 5;
protected final Verifier verifier;
protected final String body;
protected final String requestId;
public WechatPay2ValidatorForRequest(Verifier verifier, String body, String requestId) {
this.verifier = verifier;
this.body = body;
this.requestId = requestId;
}
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
/**
* 对请求做校验
* @param request
* @return
* @throws IOException
*/
public final boolean validate(HttpServletRequest request) throws IOException {
try {
validateParameters(request);
String message = buildMessage(request);
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, request.getHeader(REQUEST_ID));
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
protected final void validateParameters(HttpServletRequest request) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
for (String headerName : headers) {
header = request.getHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
String timestampStr = header;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒绝过期应答
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(HttpServletRequest request) throws IOException {
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
}
3、开启nattap内网穿透
4、编写通知的WxPayController中的方法
@Resource
private Verifier verifier; // 验签器
/**
* 接收微信回调的方法
*/
@ResponseBody
@PostMapping("/notify")
public String nativeNotify(HttpServletRequest request , HttpServletResponse response) throws IOException, GeneralSecurityException {
Gson gson = new Gson();
// 构建应答对象
Map<String,String> map = new HashMap<>();
// 接收微信返回的数据
String body = HttpUtils.readData(request);
HashMap bodyMap = gson.fromJson(body, HashMap.class);
log.info("支付通知完成后获取到的数据 body : "+ body);
// 进行签名验证
WechatPay2ValidatorForRequest validator = new WechatPay2ValidatorForRequest(
verifier, // 验签器
body, // body
(String) bodyMap.get("id")); // 支付通知的id
if(!validator.validate(request)){
log.error("通知验签失败");
response.setStatus(500);
map.put("code","ERROR");
map.put("message","通知验签失败");
return gson.toJson(map);
}
log.info("通知验签成功");
// @TODD 处理订单的加密数据, 根据解析出的数据,将订单的状态进行修改
Map<String,Object> plaintMap = wxPayService.processOrder(bodyMap);
String tradeNo = (String) plaintMap.get("out_trade_no"); // 订单号
Order byTradeNo = orderService.getByTradeNo(tradeNo); // 根据订单编号获取订单
// 更新订单的状态
orderService.updateOrderStatus(byTradeNo, SysConstant.TRADE_SUCCESS);
response.setStatus(200);
map.put("code","SUCCESS");
map.put("message","成功");
return gson.toJson(map);
}
5、支付报文解密
解密的流程:
- 1、用商户平台上设置的APIv3密钥【微信商户平台->账户设置->API安全->设置APIv3密钥】,记为key;
- 2、针对resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),取得对应的参数nonce和associated_data;
- 3、使用key, nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象;
- 4、注: AEAD_AES_256_GCM算法的接口细节,请参考rfc5116。微信支付使用的密钥key长度为32个字节,随机串nonce长度12个字节,associated_data长度小于16个字节并可能为空字符串。
WxPayServiceImpl
/**
* 加密数据解密的方法
* @param bodyMap
* @return
*/
Map<String, Object> processOrder(HashMap bodyMap) throws GeneralSecurityException;
/**
* 解密数据
* @param bodyMap
* @return
*/
@Override
public Map<String, Object> processOrder(HashMap bodyMap) throws GeneralSecurityException {
//1.密文机密操作
String plainText = decryptFromResouce(bodyMap);
//2.转成map格式
Gson gson = new Gson();
HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
return plainTextMap;
}
/**
* 因为加密方式AES方式, 解密也需要AES方式
* @param bodyMap
* @return
*/
private String decryptFromResouce(HashMap bodyMap) throws GeneralSecurityException {
// 获取通知中的resource这部分加密数据
Map<String,String> resourceMap = (Map<String, String>) bodyMap.get("resource");
// 获取密文
String ciphertext = resourceMap.get("ciphertext");
// 随机串获取
String nonce = resourceMap.get("nonce");
// 获取附加数据
String associatedData = resourceMap.get("associated_data");
System.out.println("ciphertext = " + ciphertext);
// 使用APIV3秘钥进行解密
AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); // 秘钥工具类 aesUtil
// 使用该工具进行解密
String plainText = aesUtil.decryptToString(
associatedData.getBytes(StandardCharsets.UTF_8), // 附加数据
nonce.getBytes(StandardCharsets.UTF_8), // 随机串
ciphertext); // 密文
return plainText;
}
OrderServiceImpl
Order getByTradeNo(String out_trade_no);
void updateOrderStatus(Order order, String tradeSuccess);
//更新订单信息
@Override
public void updateOrderStatus(Order order, String tradeSuccess) {
order.setStatus(tradeSuccess);
order.setUpdateTime(new Timestamp(new Date().getTime()));
orderMapper.updateById(order);
}
//根据订单编号获取订单
@Override
public Order getByTradeNo(String out_trade_no) {
//定义查询条件
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",out_trade_no);
Order order = orderMapper.selectOne(queryWrapper);
return order;
}
🚗 查询订单的状态
🚬 二维码关闭
//设置一个周期定时执行的任务
interval = setInterval(function () {
//定时查询订单的状态
$.post("wxpay/queryOrderStatus/" + orderNo, function (data) {
//判断订单状态是否成功
if (data.result.status == "TRADE_SUCCESS") {
//清除定时周期任务
clearInterval(interval);
//关闭open的二维码
$(".layui-layer-content").hide().next().hide().next().hide().next().hide();
layer.msg("交易成功")
}
})
}, 5000)
🚬 查询订单的控制器
/**
* 查询订单的状态
*/
@PostMapping("/queryOrderStatus/{orderNo}")
@ResponseBody
public BaseResult queryOrderStatus(@PathVariable String orderNo){
Order order = wxPayService.queryOrderStatus(orderNo);
if(order != null){
return BaseResult.success("查询成功",order);
}
return BaseResult.fail("查询失败");
}
🚬 查询订单的服务
OrderServiceImpl
WxPayServiceImpl
Order queryOrderStatus(String orderNo);
@Override
public Order queryOrderStatus(String orderNo) {
Order order = orderService.getByTradeNo(orderNo);
return order;
}
Order getByTradeNo(String out_trade_no);
//根据订单编号获取订单
@Override
public Order getByTradeNo(String out_trade_no) {
//定义查询条件
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",out_trade_no);
Order order = orderMapper.selectOne(queryWrapper);
return order;
}
🚗 用户关闭订单(取消订单)
/**
* 用户关闭订单(取消订单)
*/
@ResponseBody
@PostMapping("/cancelOrder/{orderNo}")
public BaseResult cancel(@PathVariable String orderNo) throws IOException {
log.info("取消订单开始...");
wxPayService.cancelOrder(orderNo);
return BaseResult.success("订单取消成功");
}
WxPayServiceImpl
OrderServiceImpl
void cancelOrder(String orderNo) throws IOException;
/**
* 取消订单
* @param orderNo
*/
@Override
public void cancelOrder(String orderNo) throws IOException {
log.info("service 取消订单开始");
// 创建远程的请求对象
String url = String.format(SysConstant.CLOSE_ORDER_BY_NO,orderNo); // 拼接定义的关闭订单地址 (符号转换)
url = wxPayConfig.getDomain().concat(url);
HttpPost httpPost = new HttpPost(url);
// 组装json的请求体
Gson gson = new Gson();
Map<String,String> paramsMap = new HashMap<>();
paramsMap.put("mchid",wxPayConfig.getMchId()); // 商户号
String jsonParams = gson.toJson(paramsMap); // 传入直连商户号 和 订单号 放到了url中 rul放到了 httpPost中 两个参数都存在
// 请求参数设置到请求对象中
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept","application/json");
// 完成签名并执行
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
//获取相应信息
int statusCode = response.getStatusLine().getStatusCode();
if(statusCode == 200){
log.info("取消成功, 返回状态码200");
}else if(statusCode == 204){
log.info("取消成功, 没有body返回体 ,状态码 204");
}else{
log.info("取消订单失败 , 状态码为 "+ statusCode);
throw new IOException("请求失败 ,取消订单异常");
}
} finally {
response.close();
}
// 更新商户的订单状态
Order order = orderService.getByTradeNo(orderNo);
orderService.updateOrderStatus(order,SysConstant.CANCEL_SUCCESS);
}
🚲 退款服务
/**
* 退款服务
*/
@ResponseBody
@PostMapping("/refund/{orderNo}")
public BaseResult refunds(@PathVariable String orderNo) throws IOException {
//调用service的退款服务
log.info("开始退款");
wxPayService.refund(orderNo);
return BaseResult.success("退款调用成功");
}
WxPayServiceImpl
void refund(String orderNo) throws IOException;
/**
* 退款服务
*/
@Override
public void refund(String orderNo) throws IOException {
//根据订单id查询订单信息
Order order = orderService.getByTradeNo(orderNo);
//准备请求的url
String url = wxPayConfig.getDomain().concat(SysConstant.DOMESTIC_REFUNDS);
HttpPost httpPost = new HttpPost(url);
//封装请求参数
Gson gson = new Gson();
Map<String ,Object> paramMap = new HashMap<>();
paramMap.put("out_trade_no",orderNo); //订单的编号
paramMap.put("out_refund_no",orderNo); //退款的编号
//设置通知地址,退款后回调通知
paramMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(SysConstant.REFUND_NOTIFY));
//设置金额的参数
Map amountMap = new HashMap();
amountMap.put("refund",order.getTotalAmount()); //设置退款金额
amountMap.put("total",order.getTotalAmount()); //原订单金额
amountMap.put("currency","CNY"); //退款的币种
paramMap.put("amount",amountMap);
//将参数转成json
String jsonParams = gson.toJson(paramMap);
//将参数再次封装到StringEntity
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept","application/json");
//发起退款请求,对请求的内容进行签名验证
CloseableHttpResponse response = wxPayClient.execute(httpPost);
//解析相应
try {
HttpEntity httpEntity = response.getEntity();
String bodyString = EntityUtils.toString(httpEntity);
int statusCode = response.getStatusLine().getStatusCode(); // 获取相应码
if (statusCode == 200){
log.info("退款申请成功...等待退款结果"+ bodyString);
}else if (statusCode == 204){
log.info("退款申请成功");
}else{
log.info("退款申请失败");
throw new RuntimeException("退款异常 , 响应码是"+statusCode + " , 相应内容是 "+bodyString );
}
} finally {
response.close();
}
//更新订单状态
orderService.updateOrderStatus(order,SysConstant.REFUND_PROCESSING);
}
🚀 退款通知
/**
* 退款通知
*/
@ResponseBody
@PostMapping("/refundsNotify")
public String refundsNotify(HttpServletRequest request , HttpServletResponse response) throws IOException, GeneralSecurityException {
//1.获取请求参数
Gson gson = new Gson();
Map<String,String> map = new HashMap<>();
String data = HttpUtils.readData(request);
HashMap dataMap = gson.fromJson(data, HashMap.class);
//做请求签名的校验
String requestId = (String) dataMap.get("id");
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier,data,requestId);
if(!wechatPay2ValidatorForRequest.validate(request)){
log.error("退款通知验证失败..");
response.setStatus(500);
map.put("code","fail");
map.put("message","退款通知验签失败");
return gson.toJson(map);
}
//验签成功
Map<String,Object> plaintTextMap = wxPayService.processRefund(dataMap);
//获取明文中的订单号
String tradeNo = (String) plaintTextMap.get("out_trade_no");
//根据订单号查询order
Order order = orderService.getByTradeNo(tradeNo);
//更新数据库中的订单状态
orderService.updateOrderStatus(order,SysConstant.REFUD_SUCCESS);
//做成功的应答
response.setStatus(200);
map.put("code","SUCCESS");
map.put("message","通知成功");
return gson.toJson(map);
}
WxPayServiceImpl
Map<String, Object> processRefund(HashMap dataMap) throws GeneralSecurityException;
/**
* 解密数据
* @param dataMap
* @return
*/
@Override
public Map<String, Object> processRefund(HashMap dataMap) throws GeneralSecurityException {
//1.转换相应中的密文
String decryptText = decryptFromResouce(dataMap);
//2.把转后的明文数据转成map
Gson gson = new Gson();
HashMap plaintTextmap = gson.fromJson(decryptText, HashMap.class);
return plaintTextmap;
}