微信支付
整个流程使用到的组件代码:
链接:https://pan.baidu.com/s/1v5415tEtetxdsp4o7HMy5A
提取码:ys87
二维码创建
利用qrious制作二维码插件。
qrious是一款基于HTML5 Canvas的纯JS二维码生成插件。通过qrious.js可以快速生成各种二维码,你可以控制二维码的尺寸颜色,还可以将生成的二维码进行Base64编码。
qrious.js二维码插件的可用配置参数如下:
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
background | String | “white” | 二维码的背景颜色。 |
foreground | String | “black” | 二维码的前景颜色。 |
level | String | “L” | 二维码的误差校正级别(L, M, Q, H)。 |
mime | String | “image/png” | 二维码输出为图片时的MIME类型。 |
size | Number | 100 | 二维码的尺寸,单位像素。 |
value | String | “” | 需要编码为二维码的值,一般是跳转url |
下面的代码即可生成一张二维码
<html>
<head>
<title>二维码入门小demo</title>
</head>
<body>
<img id="qrious">
<script src="qrious.js"></script>
<script>
var qr = new QRious({
element:document.getElementById('qrious'),
size:250,
level:'H',
value:'http://www.itheima.com'
});
</script>
</body>
</html>
运行效果:
微信扫码支付
微信扫码支付申请
微信扫码支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。
申请步骤:
第一步:注册公众号(类型须为:服务号)
请根据营业执照类型选择以下主体注册:个体工商户| 企业/公司| 政府| 媒体| 其他类型。
需要企业,才可微信支付。
第二步:认证公众号
公众号认证后才可申请微信支付,认证费:300元/次。
第三步:提交资料申请微信支付
登录公众平台,点击左侧菜单【微信支付】,开始填写资料等待审核,审核时间为1-5个工作日内。
第四步:开户成功,登录商户平台进行验证
资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证。
第五步:在线签署协议
本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。
有“传智播客”的微信支付账号,学员无需申请。
开发文档
微信支付接口调用的整体思路:
按API要求组装参数,以XML方式发送(POST)给微信支付接口(URL),微信支付接口也是以XML方式给予响应。程序根据返回的结果(其中包括支付URL)生成二维码或判断订单状态。
在线微信支付开发文档:
https://pay.weixin.qq.com/wiki/doc/api/index.html
Native二维码扫码支付。
”统一下单”和”查询订单”两组API
1. appid:微信公众账号或开放平台APP的唯一标识
2. mch_id:商户号 (配置文件中的partner)
3. partnerkey:商户密钥
4. sign:数字签名, 根据微信官方提供的密钥和一套算法生成的一个加密信息, 就是为了保证交易的安全性
微信扫码支付模式介绍
模式一
商家二维码不过时。
业务流程说明:
1.商户后台系统根据微信支付规定格式生成二维码(规则见下文),展示给用户扫码。
2.用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
3.微信支付系统收到客户端请求,发起对商户后台系统支付回调URL的调用。调用请求将带productid和用户的openid等参数,并要求商户系统返回交数据包,详细请见"本节3.1回调数据输入参数"
4.商户后台系统收到微信支付系统的回调请求,根据productid生成商户系统的订单。
5.商户系统调用微信支付【统一下单API】请求下单,获取交易会话标识(prepay_id)
6.微信支付系统根据商户系统的请求生成预支付交易,并返回交易会话标识(prepay_id)。
7.商户后台系统得到交易会话标识prepay_id(2小时内有效)。
8.商户后台系统将prepay_id返回给微信支付系统。返回数据见"本节3.2回调数据输出参数"
9.微信支付系统根据交易会话标识,发起用户端授权支付流程。
10.用户在微信客户端输入密码,确认支付后,微信客户端提交支付授权。
11.微信支付系统验证后扣款,完成支付交易。
12.微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
13.微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
14.未收到支付通知的情况,商户后台系统调用【查询订单API】。
15.商户确认订单已支付后给用户发货。
模式二
商家二维码会过时。
业务流程说明:
1.商户后台系统根据用户选购的商品生成订单。
2.用户确认支付后调用微信支付【统一下单API】生成预支付交易;
3.微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
4.商户后台系统根据返回的code_url生成二维码。
5.用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
6.微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
7.用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
8.微信支付系统根据用户授权完成支付交易。
9.微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
10.微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
11.未收到支付通知的情况,商户后台系统调用【查询订单API】。
12.商户确认订单已支付后给用户发货。
微信支付SDK
微信支付提供了SDK, 下载后打开源码,install到本地仓库。
安装SDK,jar包。
使用微信支付SDK(开发工具包), 在maven工程中引入依赖
<!-- 微信支付 -->
<wxpay.version>0.0.3</wxpay.version>
<!-- 微信支付 -->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>${wxpay.version}</version>
</dependency>
我们主要会用到微信支付SDK的以下功能:
@Test
public void test() throws Exception {
//生成随机字符串
String s = WXPayUtil.generateNonceStr();
System.out.println("随机字符串:"+s);
//将map转成xml字符串
Map<String, String> param = new HashMap<>();
param.put("id","1");
param.put("title","吴泽强");
String mapToXml = WXPayUtil.mapToXml(param);
System.out.println("map转成xml字符串:\n"+mapToXml);
//将map转成字符串,并且生成签名
String partnerKey = "wzq"; //私钥
String generateSignedXml = WXPayUtil.generateSignedXml(param, partnerKey);
System.out.println("xml字符串带有签名:\n"+generateSignedXml);
//将XML字符串转成map
Map<String,String> mapResult = WXPayUtil.xmlToMap(mapToXml);
System.out.println("xml字符串转成map"+mapResult);
}
项目实战
支付可单独是一个微服务。
依赖:
<!-- 微信支付 -->
<wxpay.version>0.0.3</wxpay.version>
<!-- 微信支付 -->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>${wxpay.version}</version>
</dependency>
商户到腾讯官网申请才有的配置application.yml:
#微信支付信息配置,这里是黑马提供的
weixin:
#应用id
appid: wx8397f8696b538317
#商户id
partner: 1473426802
#私钥
partnerkey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
#支付回调地址,自己本机模拟,花生壳
notifyurl: http://c3009841m5.zicp.vip:动态端口/weixin/pay/notify/url
appid: 微信公众账号或开放平台APP的唯一标识
partner:财付通平台的商户账号
partnerkey:财付通平台的商户密钥
notifyurl: 回调地址
默认二维码2小时失效,具体官网文档有说。
统一下单api
在支付页面上生成支付二维码,并显示订单号和金额
用户拿出手机,打开微信扫描页面上的二维码,然后在微信中完成支付
官网文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1
二维码只能支付成功一次,成功后则失效,默认2小时未支付,二维码无效,具体修改无效时长看文档。结合HttpClient工具类实现。我们传给微信服务器和微信服务器传过来都是xml格式的,有工具类可以转。
dto:
参数dto和返回dto,看情况使用。
/**
* Title:
* Description:创建二维码需要的参数
* @author WZQ
* @version 1.0.0
* @date 2020/3/13
*/
public class NativeParamDto implements Serializable {
private static final long serialVersionUID = -2376427360982551378L;
//客户端自定义订单编号
private String outTradeNo;
//交易金额(单位:分)
private String totalFee;
//set.get..
}
/**
* Title:
* Description:创建二维码成功的结果
* @author WZQ
* @version 1.0.0
* @date 2020/3/13
*/
public class NativeDto implements Serializable {
private static final long serialVersionUID = -2376427360982551378L;
//客户端自定义订单编号
private String outTradeNo;
//交易金额(单位:分)
private String totalFee;
//二维码地址
private String codeUrl
//set.get..
}
servie:
/**
* 创建二维码
* @param paramMap 客户端自定义订单编号;交易金额,单位:分
* @return
*/
Map<String, String> createNative(Map<String, String> paramMap);
import com.changgou.commons.utils.HttpClient;
import com.changgou.service.pay.service.WeixinPayService;
import com.github.wxpay.sdk.WXPayUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class WeixinPayServiceImpl implements WeixinPayService {
@Value("${weixin.appid}")
private String appid;
@Value("${weixin.partner}")
private String partner;
@Value("${weixin.partnerkey}")
private String partnerkey;
@Value("${weixin.notifyurl}")
private String notifyurl;
/**
* 创建二维码
* @param parameterMap 客户端自定义订单编号;交易金额(单位:分)
* @return
*/
@Override
public Map<String,String> createNative(Map<String, String> parameterMap){
//客户端自定义订单编号;交易金额(单位:分)
String outTradeNo = parameterMap.get("no");
String totalFee = parameterMap.get("money");
try {
//1、封装参数
Map<String,String> paramMap = new HashMap<>();
paramMap.put("appid", appid); //应用ID
paramMap.put("mch_id", partner); //商户ID号
paramMap.put("nonce_str", WXPayUtil.generateNonceStr()); //随机数
paramMap.put("body", "畅购"); //订单描述
paramMap.put("out_trade_no", outTradeNo); //商户订单号
paramMap.put("total_fee", totalFee); //交易金额,单位分
paramMap.put("spbill_create_ip", "127.0.0.1"); //终端IP,请求的地址
paramMap.put("notify_url", notifyurl); //回调地址
paramMap.put("trade_type", "NATIVE"); //交易类型,扫码
//2、将参数转成xml字符,并携带签名
String paramXml = WXPayUtil.generateSignedXml(paramMap, partnerkey);
///3、执行请求,微信统一下单api
HttpClient httpClient = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
httpClient.setHttps(true);
httpClient.setXmlParam(paramXml);
httpClient.post(); //执行
//4、获取参数
String content = httpClient.getContent();
Map<String, String> stringMap = WXPayUtil.xmlToMap(content);
System.out.println("stringMap:"+stringMap);
//5、获取部分页面所需参数
Map<String,String> dataMap = new HashMap<String,String>();
dataMap.put("code_url", stringMap.get("code_url"));
dataMap.put("out_trade_no", outTradeNo);
dataMap.put("total_fee", totalFee);
return dataMap;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
controller:
@RestController
@RequestMapping(value = "/weixin/pay")
@CrossOrigin
public class WeixinPayController {
@Autowired
private WeixinPayService weixinPayService;
/***
* 创建二维码
* 参数是订单号和金额,分为单位
* @return
*/
@RequestMapping(value = "/create/native")
public ResponseResult<Map<String,String>> createNative(@RequestParam Map<String, String> paramMap){
Map<String,String> resultMap = weixinPayService.createNative(paramMap);
return new ResponseResult<Map<String,String>>(true, StatusCode.OK,"创建二维码预付订单成功!",resultMap);
}
}
测试 http://localhost:18092/weixin/pay/create/native?no=1714080902133&money=1:
这里是表单传参的,json的话也可以,自定义好dto即可。
把code_url的地址显示到qrious组件的前端页面,存放到value对应的值即是一个二维码
支付信息回调
接口分析
每次实现支付之后,微信支付都会将用户支付结果返回到指定路径,而指定路径是指创建二维码的时候填写的notifyurl
参数,响应的数据以及相关文档参考一下地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8
返回参数分析
通知参数如下:
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | 是 | String(16) | SUCCESS | SUCCESS |
返回信息 | return_msg | 是 | String(128) | OK | OK |
以下字段在return_code为SUCCESS的时候有返回
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
公众账号ID | appid | 是 | String(32) | wx8888888888888888 | 微信分配的公众账号ID(企业号corpid即为此appId) |
业务结果 | result_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL |
商户订单号 | out_trade_no | 是 | String(32) | 1212321211201407033568112322 | 商户系统内部订单号 |
微信支付订单号 | transaction_id | 是 | String(32) | 1217752501201407033233368018 | 微信支付订单号 |
响应分析
回调地址接收到数据后,需要响应信息给微信服务器,告知已经收到数据,不然微信服务器会再次发送4次请求推送支付信息。
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | 是 | String(16) | SUCCESS | 请按示例值填写 |
返回信息 | return_msg | 是 | String(128) | OK | 请按示例值填写 |
举例如下:
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>
回调接收数据实现
WeixinPayController, 添加回调方法,代码如下:
/***
* 支付回调,数据流数据
* @param request
* @return
*/
@RequestMapping(value = "/notify/url")
public String notifyUrl(HttpServletRequest request){
InputStream inStream;
try {
//读取支付回调数据
inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024]; //缓存区
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
outSteam.close();
inStream.close();
// 将支付回调数据转换成xml字符串
String result = new String(outSteam.toByteArray(), "utf-8");
//将xml字符串转换成Map结构
Map<String, String> map = WXPayUtil.xmlToMap(result);
//响应数据设置
Map<String,String> respMap = new HashMap<>();
respMap.put("return_code","SUCCESS");
respMap.put("return_msg","OK");
return WXPayUtil.mapToXml(respMap);
} catch (Exception e) {
e.printStackTrace();
//记录错误日志
}
return null;
}
本地机可以使用花生壳来模拟外网访问我们的本地机服务。花生壳服务器通过该软件和账号把一个域名和本地机的ip绑定在一起,实现内网穿透。通过随机域名可外网访问本地机。
外网访问:http://c3009841m5.zicp.vip:动态端口/weixin/pay/notify/url
查询订单api
官网文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2
通过HttpClient工具类实现对远程支付接口的调用。具体参数参见“查询订单”API, 我们在controller方法中轮询调用查询订单(间隔3秒),当返回状态为SUCCESS时,我们会在controller方法返回结果。前端代码收到结果后跳转到成功页面。默认统一下单api是有回调地址notifyurl,我们自己设定的地址,一般微信服务器支付成功后,会回调该地址过来,默认5次,该回调带有订单状态。如果出事故,则需要我们自己用查询订单api来查询订单的状态。
service:
/***
* 查询订单状态
* @param outTradeNo : 客户端自定义订单编号
* @return
*/
Map<String, String> queryPayStatus(String outTradeNo);
/***
* 查询订单状态
* @param outTradeNo : 客户端自定义订单编号
* @return
*/
@Override
public Map<String,String> queryPayStatus(String outTradeNo) {
try {
//1.封装参数
Map<String,String> param = new HashMap<>();
param.put("appid",appid); //应用ID
param.put("mch_id",partner); //商户号
param.put("out_trade_no",outTradeNo); //商户订单编号
param.put("nonce_str",WXPayUtil.generateNonceStr()); //随机字符
//2、将参数转成xml字符,并携带签名
String paramXml = WXPayUtil.generateSignedXml(param,partnerkey);
//3、发送请求
HttpClient httpClient = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
httpClient.setHttps(true);
httpClient.setXmlParam(paramXml);
httpClient.post();
//4、获取返回值,并将返回值转成Map
String content = httpClient.getContent();
return WXPayUtil.xmlToMap(content);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
controller:
/***
* 查询支付状态
* @param outTradeNo 自定义订单id,不是微信的id
* @return
*/
@GetMapping(value = "/status/query")
public ResponseResult<Map<String,String>> queryStatus(String outTradeNo){
Map<String,String> resultMap = weixinPayService.queryPayStatus(outTradeNo);
return new ResponseResult<Map<String,String>>(true,StatusCode.OK,"查询状态成功!",resultMap);
}
测试:
关闭订单api
官网文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_3
用户如果一直未支付,或者取消订单,我们除了要对自定义数据库取消支付状态回滚库存外,还需要向微信服务器请求关闭订单api,让二维码失效(默认2小时失效)。
关闭支付订单,但在关闭之前,需要先关闭微信支付,防止中途用户支付。
修改支付微服务的WeixinPayService,添加关闭支付方法,代码如下:
/***
* 关闭支付
* @param orderId
* @return
*/
Map<String,String> closePay(Long orderId) throws Exception;
修改WeixinPayServiceImpl,实现关闭微信支付方法,代码如下:
/***
* 关闭微信支付
* @param orderId
* @return
* @throws Exception
*/
@Override
public Map<String, String> closePay(Long orderId) throws Exception {
//参数设置
Map<String,String> paramMap = new HashMap<String,String>();
paramMap.put("appid",appid); //应用ID
paramMap.put("mch_id",partner); //商户编号
paramMap.put("nonce_str",WXPayUtil.generateNonceStr());//随机字符
paramMap.put("out_trade_no",String.valueOf(orderId)); //商家的唯一编号
//将Map数据转成XML字符
String xmlParam = WXPayUtil.generateSignedXml(paramMap,partnerkey);
//确定url
String url = "https://api.mch.weixin.qq.com/pay/closeorder";
//发送请求
HttpClient httpClient = new HttpClient(url);
//https
httpClient.setHttps(true);
//提交参数
httpClient.setXmlParam(xmlParam);
//提交
httpClient.post();
//获取返回数据
String content = httpClient.getContent();
//将返回数据解析成Map
return WXPayUtil.xmlToMap(content);
}
controller:
/***
* 关闭订单状态
* @param outTradeNo 自定义订单id,不是微信订单的id
* @return
*/
@GetMapping(value = "/status/close")
public ResponseResult<Map<String,String>> closePay(String outTradeNo){
Map<String,String> resultMap = weixinPayService.closePay(outTradeNo);
return new ResponseResult<Map<String,String>>(true,StatusCode.OK,"关闭订单成功!",resultMap);
}
MQ处理支付回调状态
支付系统是独立于其他系统的服务,不做相关业务逻辑操作,只做支付处理,所以回调地址接收微信服务返回的支付状态后,立即将消息发送给RabbitMQ,订单系统再监听支付状态数据,根据状态数据做出修改订单状态或者删除订单操作。
发送MQ消息
支付微服务回调后发送消息
支付微服务集成RabbitMQ,添加依赖:
<!--加入ampq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在后台手动创建队列,并绑定队列(进入rabbitmq主页,手动创建)。如果使用程序创建队列,可以按照如下方式实现。
修改application.yml,配置支付队列和交换机信息,代码如下:
spring:
rabbitmq:
host: 192.168.169.140 #ip地址
port: 5672 #端口
username: guest #账号
password: guest #密码
#设置order交换机和order队列名称
mq:
pay:
exchange:
order: exchange.order
queue:
order: queue.order
routing:
orderkey: queue.order
创建队列以及交换机并让队列和交换机绑定,添加到MQConfig,添加如下代码:
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import javax.annotation.Resource;
import java.util.Objects;
/**
* Title:rabbitmq配置
* Description:创建队列和交换机
* @author WZQ
* @version 1.0.0
* @date 2020/3/13
*/
@Configuration
public class MQConfig {
//读取配置文件的内容
@Resource
private Environment env;
/***
* 创建DirectExchange交换机
* @return
*/
@Bean
public DirectExchange basicExchange(){
//true:是否实体化,false:是否自动删除
return new DirectExchange(env.getProperty("mq.pay.exchange.order"), true,false);
}
/***
* 创建队列
* @return
*/
@Bean(name = "queueOrder")
public Queue queueOrder(){
return new Queue(Objects.requireNonNull(env.getProperty("mq.pay.queue.order")), true);
}
/****
* 队列绑定到交换机上
* @return
*/
@Bean
public Binding basicBinding(){
return BindingBuilder.bind(queueOrder()).to(basicExchange()).with(env.getProperty("mq.pay.routing.orderkey"));
}
}
使用:
@Value("${mq.pay.exchange.order}")
private String exchange;
@Value("${mq.pay.queue.order}")
private String queue;
@Value("${mq.pay.routing.key}")
private String routing;
@Autowired
private RabbitTemplate rabbitTemplate;
/***
* 支付回调
* @param request
* @return
*/
@RequestMapping(value = "/notify/url")
public String notifyUrl(HttpServletRequest request){
InputStream inStream;
try {
//读取支付回调数据
inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
outSteam.close();
inStream.close();
// 将支付回调数据转换成xml字符串
String result = new String(outSteam.toByteArray(), "utf-8");
//将xml字符串转换成Map结构
Map<String, String> map = WXPayUtil.xmlToMap(result);
//将消息发送给RabbitMQ
rabbitTemplate.convertAndSend(exchange,routing, JSON.toJSONString(map));
//响应数据设置
Map respMap = new HashMap();
respMap.put("return_code","SUCCESS");
respMap.put("return_msg","OK");
return WXPayUtil.mapToXml(respMap);
} catch (Exception e) {
e.printStackTrace();
//记录错误日志
}
return null;
}
监听MQ消息
处理订单状态。在订单微服务中,我们需要监听MQ支付状态消息,并实现订单数据操作。
订单微服务集成RabbitMQ,再监听队列消息。
在pom.xml中引入如下依赖:
<!--加入ampq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在application.yml中加入配置,代码如下:
spring:
rabbitmq:
host: 192.168.169.140 #ip地址
port: 5672 #端口
username: guest #账号
password: guest #密码
#设置队列名称,跟发送消息的队列名字一致
mq:
pay:
queue:
order: queue.order
监听消息修改订单
在订单微服务于中创建consumer.OrderPayMessageListener,在该类中consumeMessage方法,用于监听消息,并根据支付状态处理订单,代码如下:
@Component
@RabbitListener(queues = {"${mq.pay.queue.order}"})
public class OrderPayMessageListener {
@Resource
private OrderService orderService;
/***
* 接收消息
*/
@RabbitHandler
public void consumeMessage(String msg){
//将数据转成Map
Map<String,String> result = JSON.parseObject(msg,Map.class);
//return_code=SUCCESS
String return_code = result.get("return_code");
//业务结果
String result_code = result.get("result_code");
//状态码 return_code=SUCCESS/FAIL,修改订单状态
if(return_code.equalsIgnoreCase("success") ){
//获取订单号
String outTradeNo = result.get("out_trade_no");
//业务结果,支付成功的
if(result_code.equalsIgnoreCase("success")){
if(outTradeNo!=null){
//修改订单状态根据out_trade_no,具体逻辑业务,看情况
//transaction_id微信支付的订单号,time_end是结算时间
orderService.updateStatus(outTradeNo, result.get("time_end"),result.get("transaction_id"));
}
}else{
//需要调用微信服务器关闭订单
//订单删除,具体逻辑业务,看情况
orderService.deleteOrder(outTradeNo);
}
}
}
}
其它微信api接口,详看:https://pay.weixin.qq.com/wiki/doc/api/native.php
RabbitMQ延时消息队列
延时队列介绍
延时队列即放置在该队列里面的消息是不需要立即消费的,而是等待一段时间之后取出消费。
那么,为什么需要延迟消费呢?我们来看以下的场景:
网上商城下订单后30分钟后没有完成支付,取消订单(如:淘宝、去哪儿网);
系统创建了预约之后,需要在预约时间到达前一小时提醒被预约的双方参会;
系统中的业务失败之后,需要重试;
这些场景都非常常见,我们可以思考,比如第二个需求,系统创建了预约之后,需要在预约时间到达前一小时提醒被预约的双方参会。那么一天之中肯定是会有很多个预约的,时间也是不一定的,假设现在有1点 2点 3点 三个预约,如何让系统知道在当前时间等于0点 1点 2点给用户发送信息呢,是不是需要一个轮询,一直去查看所有的预约,比对当前的系统时间和预约提前一小时的时间是否相等呢?这样做非常浪费资源而且轮询的时间间隔不好控制。如果我们使用延时消息队列呢,我们在创建时把需要通知的预约放入消息中间件中,并且设置该消息的过期时间,等过期时间到达时再取出消费即可。
Rabbitmq实现延时队列一般而言有两种形式:
第一种方式:利用两个特性: Time To Live(TTL)、Dead Letter Exchanges(DLX)[A队列过期->转发给B队列]
第二种方式:利用rabbitmq中的插件x-delay-message
TTL DLX实现延时队列
TTL DLX介绍
TTL
RabbitMQ可以针对队列设置x-expires(则队列中所有的消息都有相同的过期时间)或者针对Message设置x-message-ttl(对消息进行单独设置,每条消息TTL可以不同),来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)
Dead Letter Exchanges(DLX)
RabbitMQ的Queue可以配置x-dead-letter-exchange和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由转发到指定的队列。
x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送
DLX延时队列实现
还是在订单微服务中,当用户生成订单支付状态后,如果半小时未支付,则取消支付,向微信服务器请求关闭订单api。
配置队列
#设置交换机和队列名称
mq:
pay:
exchange:
order: exchange.order
cancelOrder: dlx.exchange
queue:
order: queue.order
cancelOrder: queue.listener
cancelOrderDelay: queue.delay
routing:
orderkey: queue.order
cancelOrderkey: queue.listener
队列创建
创建2个队列,用于接收消息的叫延时队列DelayQueue,用于转发消息的队列叫ListenerQueue,同时创建一个交换机,代码如下:
/**
* Title:延时队列配置
* Description:创建2个队列,利用rabbit队列超时机制
* @author WZQ
* @version 1.0.0
* @date 2020/3/14
*/
@Configuration
public class DelayQueueConfig {
//读取配置文件的内容
@Resource
private Environment env;
/**
* 创建queue1,没有被监听,延时队列
* 消息过期了发送给死信队列,死信队列绑定queue2
*/
@Bean
public Queue orderDelayQueue(){
return QueueBuilder
.durable(Objects.requireNonNull(env.getProperty("mq.pay.queue.cancelOrderDelay"))
// 延时队列过期的消息会进入给死信队列(没有被读的消息),死信队列的消息就发送到queue2中
// 这里就需要死信队列的交换机和队列,规定是死信路由key的就路由到queue2
.withArgument("x-dead-letter-exchange", env.getProperty("mq.pay.exchange.cancelOrder")) //死信交换机
.withArgument("x-dead-letter-routing-key", Objects.requireNonNull(env.getProperty("mq.pay.queue.cancelOrder")) //交换机绑定路由消息到queue2
.build();
}
/**
* 创建queue2,普通有监听的队列
*/
@Bean
public Queue orderListenerQueue(){
return new Queue(Objects.requireNonNull(env.getProperty("mq.pay.queue.cancelOrder"),true);
}
/**
* 创建交换机
*/
@Bean
public Exchange orderListenerExchange(){
return new DirectExchange(env.getProperty("mq.pay.exchange.cancelOrder"));
}
/**
* 队列queue2绑定交换机
*/
@Bean
public Binding orderListenerBinding(){
return BindingBuilder.bind(orderListenerQueue()).to(orderListenerExchange()).with(Objects.requireNonNull(env.getProperty("mq.pay.queue.cancelOrderkey")).noargs();
}
}
启动类添加
@EnableRabbit
发送消息
在生成订单,获取支付状态的service中,加入发送消息代码
@Resource
private RabbitTemplate rabbitTemplate;
//读取配置文件的内容
@Resource
private Environment env;
//传订单id过去
rabbitTemplate.convertAndSend(env.getProperty("mq.pay.queue.cancelOrderkey"),
(Object) order.getId(), new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//设置延时
message.getMessageProperties().setExpiration("1800000");//30*60*1000,半小时
return message;
}
});
监听实现
订单微服务中监听订单支付状态
@Component
@RabbitListener(queues = DelayQueueConfig.LISTENER_QUEUE)
public class OrderDelayMessageListener {
@Autowired
private OrderService orderService;
@Autowired
private WeixinPayFeign weixinPayFeign;
/***
* 读取消息,消息是id
* 关闭支付,再关闭订单
* @param message
*/
@RabbitHandler
public void consumeMessage(String message){//@Payload Object message消息是实体类的json数据
//读取消息,消息封装类,自定义,订单号
//MessageStatus messageStatus = JSON.parseObject(message,MessageStatus.class);
//查询订单状态,确定未支付
selectById(message);
//如果订单未支付
//关闭支付,feign调用支付微服务的关闭订单接口
Result closeResult = weixinPayFeign.closePay(message);
Map<String,String> closeMap = (Map<String, String>) closeResult.getData();
if(closeMap!=null && closeMap.get("return_code").equalsIgnoreCase("success") &&
closeMap.get("result_code").equalsIgnoreCase("success") ){
//关闭订单
OrderService.closeOrder(username);
//库存回滚
//...
}
}
}