微服务商城系统(十四)微信支付
文章目录一、支付微服务1、微信支付 API2、HttpClient 工具类3、支付微服务搭建二、微信支付二维码生成三、检测支付状态一、支付微服务1、微信支付 API 微信支付提供了 SDK 和 Demo(然而我并没有找到 sdk,可能微信团队已经把它上传到 Maven 中了):使用微信支付SD,在 common 工程的 pom.xml 中中引入依赖&l
文章目录
代码链接: https://github.com/betterGa/ChangGou
一、支付微服务
1、微信支付 API
微信支付提供了 SDK 和 Demo(然而我并没有找到 sdk,可能微信团队已经把它上传到 Maven 中了):
使用微信支付SDK,在 common 工程的 pom.xml 中中引入依赖
<!--微信支付-->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
我们主要会用到微信支付 SDK 的以下功能:
获取随机字符串
WXPayUtil.generateNonceStr()
MAP 转换为 XML 字符串(自动添加签名)
WXPayUtil.generateSignedXml(param, partnerkey)
XML 字符串转换为 MAP(当然,Map 也可以转化成 XML)
WXPayUtil.xmlToMap(result)
在 changgou-common工程下引入依赖:
<!--微信支付-->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
进行测试:
public class WeixinPayTest {
@Test
public void test() throws Exception {
// 生成随机字符
System.out.println("随机字符串" + WXPayUtil.generateNonceStr());
// 将 Map 转化成 XML
Map<String, String> dataMap = new HashMap<>();
dataMap.put("id", "No.1");
dataMap.put("title", "畅购商城");
dataMap.put("money", "520");
String xml = WXPayUtil.mapToXml(dataMap);
System.out.println(xml);
// (1)生成签名
System.out.println("带签名的字符串" + WXPayUtil.generateSignedXml(dataMap, "secret"));
// 将 XML 转化成 Map
System.out.println(WXPayUtil.xmlToMap(xml));
}
}
运行结果:
注意到,(1)处,WXPayUtil.generateSignedXml 方法,传 Map 和 密钥 作为参数,可以生成带 签名 的字符串,签名是这么生成的。
2、HttpClient 工具类
HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。HttpClient 已经应用在很多的项目中,比如 Apache Jakarta 上很著名的另外两个开源项目 Cactus 和 HTMLUnit 都使用了 HttpClient。
HttpClient 通俗讲就是模拟了浏览器的行为,如果我们需要在后端向某一地址提交数据获取结果,就可以使用 HttpClient.
使用 HttpClient 需要先导入依赖:
<!--httpclient支持-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
关于HttpClient(原生)具体的使用不属于我们本章的学习内容,为了简化 HttpClient 的使用,提供了工具类 HttpClient(对原生 HttpClient 进行了封装)。
HttpClient工具类代码:
public class HttpClient {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;
public boolean isHttps() {
return isHttps;
}
public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}
public String getXmlParam() {
return xmlParam;
}
public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}
public HttpClient(String url, Map<String, String> param) {
this.url = url;
this.param = param;
}
public HttpClient(String url) {
this.url = url;
}
public void setParameter(Map<String, String> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, String>();
param.put(key, value);
}
public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}
public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}
public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst) {
url.append("?");
}else {
url.append("&");
}
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}
/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet()) {
nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
}
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}
private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
@Override
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null) {
statusCode = response.getStatusLine().getStatusCode();
}
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}
public int getStatusCode() {
return statusCode;
}
public String getContent() throws ParseException, IOException {
return content;
}
}
进行测试:
public class HttpClientClass {
/**
* 测试 HttpClient 工具类的使用
*/
@Test
public void test() throws IOException {
// 发送 Http 请求
String url = "https://api.mch.weixin.qq.com/pay/orderquery";
HttpClient httpClient = new HttpClient(url);
// 发送指定参数
String xml = "<xml><name>用户</name></xml>";
httpClient.setXmlParam(xml);
// 使用 Https 协议
httpClient.setHttps(true);
// 发送请求
httpClient.post();
// 获取响应数据
String result = httpClient.getContent();
System.out.println(result);
}
}
运行结果:
3、支付微服务搭建
需要支付微服务与微信支付服务器进行对接。
创建 changgou-service-pay 工程。
创建application.yml,配置文件如下:
server:
port: 18092
spring:
application:
name: pay
main:
allow-bean-definition-overriding: true
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
#微信支付信息配置
weixin:
appid: wx8397f8696b538317
partner: 1473426802
partnerkey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
notifyurl: http://www.itcast.cn
参数说明:
- appid:微信公众账号或开放平台 APP 的唯一标识
- partner:财付通平台的商户账号
- partnerkey:财付通平台的商户密钥
- notifyurl::回调地址
提供启动类:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class PayApplication {
public static void main(String[] args) {
SpringApplication.run(PayApplication.class, args);
}
}
二、微信支付二维码生成
在支付页面上生成支付二维码,并显示订单号和金额,用户拿出手机,打开微信扫描页面上的二维码,然后在微信中完成支付。
实现思路:
通过 HttpClient 工具类实现对远程支付接口的调用。
接口链接:https://api.mch.weixin.qq.com/pay/unifiedorder
具体参数参见 “统一下单” API, 构建参数发送给统一下单的 url ,返回的信息中有支付 url,根据 url 生成二维码,显示的订单号和金额也在返回的信息中。
代码实现:
- 业务层
提供接口:
public interface WeixinPayService {
Map createNative(Map<String,String> parameterMap) throws Exception;
}
实现:
@Service
public class WeiXinPayServiceImpl implements WeixinPayService {
// 应用 ID
@Value("${weixin.appid}")
private String appid;
// 商户 ID
@Value("${weixin.partner}")
private String partner;
// 密钥
@Value("${weixin.partnerkey}")
private String partnerkey;
// 支付回调地址
@Value("${weixin.notifyurl}")
private String notifyurl;
/**
* 创建二维码
*
* @param parameterMap
* @return
*/
@Override
public Map createNative(Map<String, String> parameterMap) throws Exception {
/**
* 封装参数
*/
Map<String, String> paramMap = new HashMap<>();
// 公众账号 ID
paramMap.put("appid", appid);
// 商户号
paramMap.put("mch_id", partner.trim());
// 随机字符串
paramMap.put("nonce_str", WXPayUtil.generateNonceStr());
// 商品描述
paramMap.put("body", "畅购商城");
// 商品订单号
paramMap.put("out_trade_no", parameterMap.get("outtradeno"));
// 标价金额
paramMap.put("total_fee", parameterMap.get("totalfee"));
// 终端 IP
paramMap.put("spbill_create_ip", "127.0.0.1");
// 通知地址
paramMap.put("notify_url", notifyurl);
// 交易类型
paramMap.put("trade_type", "NATIVE");
// 传入密钥,生成的 xml 中就会带有签名 sign 信息
String xmlParameters = WXPayUtil.generateSignedXml(paramMap, partnerkey);
System.out.println("xml:"+xmlParameters);
/**
* URL
*/
String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* 提交方式
*/
HttpClient httpClient = new HttpClient(url);
httpClient.setHttps(true);
/**
* 提交参数
*/
httpClient.setXmlParam(xmlParameters);
/**
* 执行请求
*/
httpClient.post();
// 返回数据
String content = httpClient.getContent();
Map<String,String> resultMap=WXPayUtil.xmlToMap(content);
return resultMap;
}
}
- 控制层
@RestController
@RequestMapping(value = "/weixin/pay")
@CrossOrigin
public class WeiXinPayController {
@Autowired
private WeixinPayService weixinPayService;
@GetMapping(value = "/create/native")
Result createNative(@RequestParam Map<String, String> parameterMap) throws Exception {
Map resultMap = weixinPayService.createNative(parameterMap);
return new Result(true, StatusCode.OK,"创建二维码预付订单成功!",resultMap);
}
}
测试如下:
打开支付页面 /pay.html,修改 value 路径,改为响应的 code_url 的值:
这时访问 pay.html 页面,会出现付款码,金额为 0.1 元。
三、检测支付状态
当用户支付成功后跳转到成功页面:
当返回异常时跳转到错误页面:
本身服务器会给商户后台系统发状态信息,但是可能有差错,导致商户后台系统没收到,所以需要主动向服务器查询状态信息。
实现思路:通过 HttpClient 工具类实现对远程接口的调用。
接口链接:https://api.mch.weixin.qq.com/pay/orderquery
具体参数参见 “查询订单” API:
代码实现:
- 业务层
在 com.changgou.service.WeixinPayService 提供方法:
/***
* 查询订单状态
* @param out_trade_no : 客户端自定义订单编号
* @return
*/
public Map queryPayStatus(String out_trade_no);
实现:
/***
* 查询订单状态
* @param out_trade_no : 客户端自定义订单编号
* @return
*/
@Override
public Map queryPayStatus(String out_trade_no) {
/**
* 查询订单状态
* @param out_trade_no
* @return
*/
@Override
public Map queryPayStatus(String out_trade_no) {
try {
//1.封装参数
Map param = new HashMap();
param.put("appid",appid); //应用ID
param.put("mch_id",partner); //商户号
param.put("out_trade_no",out_trade_no); //商户订单编号
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;
}
}
- 控制层
/***
* 查询支付状态
* @param outtradeno
* @return
*/
@GetMapping(value = "/status/query")
public Result queryStatus(String outtradeno) {
Map<String, String> resultMap = weixinPayService.queryPayStatus(outtradeno);
return new Result(true, StatusCode.OK, "查询状态成功!", resultMap);
}
运行结果:
四、内网穿透
现在系统还有个问题需要解决:微信支付服务器需要访问本地服务器的,让外网访问到本地服务器,就需要用到内网穿透技术 NAT 。使用动态域名解析工具 花生壳 。
新建映射:
其中,内网主机即本地 IP,外网域名用的是花生壳赠送的,在 “域名列表” 中可以看到。
可以看到,提供了一个访问地址。
要在诊断无误的情况下使用:
测试访问地址:
五、支付结果通知
1、支付结果回调通知
提供控制层:
@RequestMapping(value = "/notify/url")
public String notifyUrl(HttpServletRequest request) throws Exception {
// 获取网络输入流
ServletInputStream inputStream = request.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inputStream.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
// 微信支付结果的字节数据
byte[] bytes = baos.toByteArray();
String xmlResult = new String(bytes, "utf-8");
System.out.println("微信支付结果的 xml" + xmlResult);
Map<String, String> resultMap = WXPayUtil.xmlToMap(xmlResult);
System.out.println("微信支付结果的 Map" + resultMap);
String result = "<xml>\n" +
" <return_code><![CDATA[SUCCESS]]></return_code>\n" +
" <return_msg><![CDATA[OK]]></return_msg>\n" +
"</xml>";
return result;
}
需要在 application.yml 中配置访问地址,将该方法对应的路径作为配置文件中 weixin:notifyurl 的属性值。:
进行测试:
首先需要创建订单:
把 “code_url” 放到 pay.html 中,生成付款码,用微信扫描后付款,这时,会在控制台看到输出:
注意:方法是 @RequestMapping ,如果误写成 @GetMapping ,控制台输出:
它说不支持 @PostMapping,那我就改成 @PostMapping:
可以看到,和使用 @RequestMapping 的效果是一样的,说明调用这个 notifyurl 的地方,是使用 Post 方法。
至此,在 pay 项目中就可以获取到支付结果,但是 order 项目并没有获取到结果。需要让 order 项目监听 MQ,当监听到支付成功后,需要把订单状态改为 支付成功。
2、Rabbit MQ 配置
需要先在 pay 工程中导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
(1)发送支付状态
在 application.yml 中进行配置:
mq:
pay:
exchange:
order: exchange.order
queue:
order: queue.order
routing:
key: queue.order
需要清空 Exchanges 和 Queues。
本项目中使用代码生成交换机和队列(实际企业开发应该是去 MQ 中生成的),在 pay 工程中 提供一个配置类:
@Configuration
public class MQConfig {
// 读取配置文件中的内容对象
@Autowired
private Environment environment;
// 创建队列
@Bean
public Queue orderQueue() {
return new Queue(environment.getProperty("mq.pay.queue.order"));
}
// 创建交换机
@Bean
public Exchange orderExchange() {
// 持久化,不自动删除
return new DirectExchange(environment.getProperty("mq.pay.exchange.order"), true, false);
}
// 绑定
public Binding binding(Queue queue, Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange)
.with(environment.getProperty("mq.pay.routing.key"))
.noargs();
}
}
在 WeiXinPayController 的 notifyUrl 方法中,加入 把支付结果发送给 MQ 的逻辑:
还需要在 order 工程中,同样需要配置 RabbitMQ 参数,导入 spring-boot-starter-amqp 依赖。
(2)监听
提供监听类:
@Component
@RabbitListener(queues = "${mq.pay.queue.order}")
public class OrderMessageListener {
/**
* 支付结果监听
*/
@RabbitHandler
public void getMessage(String message){
// 支付结果
Map<String,String> resultMap = JSON.parseObject(message, Map.class);
// 输出监听到的消息
System.out.println(resultMap);
// 通信标识
String returnCode=resultMap.get("return_code");
if(returnCode.equals("SUCCESS")){
// 业务结果
String resultCode = resultMap.get("result_code");
// 订单号
String outTradeNo = resultMap.get("out_trade_no");
// 支付成功
if(resultCode.equals("SUCCESS")){
}else {
// 如果支付失败,需要关闭订单,回滚库存
}
}
}
}
注意:需要先启动 pay 工程,生成付款码,然后用户进行支付,创建队列 queue.order,再启动 order 工程,监听队列。不可以同时启动两个工程,否则 生产者模块所在的 pay 工程可以正常运行,但是消费者模块所在的 order 会报错误:
org.springframework.amqp.rabbit.listener.BlockingQueueConsumer$DeclarationException: Failed to declare queue(s):[topic.man] at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.attemptPassiveDeclarations(BlockingQueueConsumer.java:700) [spring-rabbit-2.2.7.RELEASE.jar:2.2.7.RELEASE]
at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.passiveDeclarations(Blocki ngQueueConsumer.java:584) [spring-rabbit-2.2.7.RELEASE.jar:2.2.7.RELEASE]
因为此时还未付款,就不会在 rabbitmq 服务器里面创建还不存在的交换机和队列。仅限于第一次启动的时候,以后 rabbitmq 里面以及有对应的交换机和队列存在了,就不用这样做了。
付款后,可以看到 RabbitMQ 中新建队列和交换机成功了:
六、修改订单状态
接下来,需要根据支付的结果修改订单信息:
在 OrderService 中提供方法:
/**
* 修改订单状态
*
* @param outradeno
* @param paytime
* @param transcationid
*/
void updateStatus(String outradeno, String paytime, String transcationid) throws ParseException;
/**
* 删除【逻辑删除,其实是修改订单状态】订单信息,回滚库存
*
* @param outradeno
*/
void deleteOrder(String outradeno);
实现:
/**
* 修改订单状态
*
* @param outradeno
* @param paytime
* @param transcationid
*/
@Override
public void updateStatus(String outradeno, String paytime, String transcationid) throws ParseException {
// 查询订单
Order order = orderMapper.selectByPrimaryKey(outradeno);
// 时间转换
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
// 获取支付时间
Date payTime = simpleDateFormat.parse(paytime);
// 修改订单信息
order.setPayTime(payTime);
order.setPayStatus("1");
order.setTransactionId(transcationid);
orderMapper.updateByPrimaryKey(order);
}
/**
* 删除订单
* @param outradeno
*/
@Override
public void deleteOrder(String outradeno) {
// 查询订单
Order order = orderMapper.selectByPrimaryKey(outradeno);
// 支付失败
order.setOrderStatus("2");
orderMapper.updateByPrimaryKey(order);
// 回滚库存,需要调用 goods 微服务,未实现
// 微信支付服务器关闭订单,未实现
}
如果用户订单支付失败了,或者支付超时了,我们需要删除用户订单,删除订单的同时需要取消订单、回滚库存:
七、超时订单处理
现在还有个问题,如果用户下单 30 分钟后还没有支付,需要把订单取消。(超过 5 分钟取消订单,是可以实现的)
那么怎么让系统知道 30 分钟后用户是否支付成功了呢?可以轮询,但是轮询非常浪费资源,而且轮询的间隔不好控制;可以用延时队列。
Rabbit MQ 本身不支持延时队列,不过可以自己实现,有两种方式:
(1)利用 2 个特性:Time To Live( TTL)、Dead Letter Exchange(DLX)、 [A 队列过期-> 转发给 B 队列]
(2)利用 RabbitMQ 中的插件 x-delay-message
采用第一种方式来实现延时队列,实际上是用队列的超时特性:
监听 Queue2 即可。
代码实现:
@Configuration
public class QueueConfig {
// 创建 queue1
@Bean
public Queue orderDelayQueue(){
return QueueBuilder.durable("orderDelayQueue")
// queue1 消息过期,进入到死信【没被读取的消息】队列,需要绑定交换机
.withArgument("x-dead-letter-exchange","orderListenerExchange")
// queue1 消息过期,会路由到 queue2
.withArgument("x-dead-letter-routing-key","orderListenerQueue")
.build();
}
// 创建 queue2
@Bean
public Queue orderListenerQueue(){
// 持久化
return new Queue("orderListenerQueue",true);
}
@Bean
public Exchange orderListenerExchange(){
return new DirectExchange("orderListenerExchange");
}
// 绑定
@Bean
public Binding binding(Queue orderListenerQueue,Exchange orderListenerExchange){
return BindingBuilder.bind(orderListenerQueue)
.to(orderListenerExchange)
.with("orderListenerQueue")
.noargs();
}
}
下单时发送消息:
提供监听:
@Component
@RabbitListener(queues = "orderListenerQueue")
public class DelayMessageListener {
@RabbitHandler
public void delayMessage(String message) {
System.out.println("监听到的消息" + message);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("监听到消息时间" + simpleDateFormat.format(new Date()));
}
}
这时进行测试,启动 order 工程,可以看到 RabbitMQ 中生成了队列和交换机:
接下来进行测试。
先下单:
可以看到,在控制台输出:
八、关闭订单与回滚库存
在 (五)支付结果通知,队列监听方法中,当监听到消息的 result_code 不为 SUCCESS 时,说明支付失败,需要关闭订单、回滚库存。回滚库存的逻辑可以参考上一篇文章中,下单-库存变更 的逻辑,在 Redis 中,查询用户对应的订单信息,然后根据 订单 order 查询出订单明细 orderItem,就能得到订单中商品的 num 数量,最后在 sku 商品表中需要把数量加回来:
需要在 skuController 中提供库存递增的方法:
@GetMapping(value = "/asc")
public Result ascCount(@RequestParam Map<String,String> ascMap){
skuService.ascCount(ascMap);
return new Result(true,StatusCode.OK,"库存递增成功");
}
在 SkuService 接口中提供方法:
void ascCount(Map<String,String> ascMap);
实现:
@Override
public void ascCount(Map<String, String> ascMap) {
for (Map.Entry<String, String> entry : ascMap.entrySet()) {
// 商品 ID
Long id = Long.parseLong(entry.getKey());
// 数量
Integer num = Integer.parseInt(entry.getValue());
/**
* 使用行级锁防止超卖,通过数据库的事务特性保证数据原子性
*/
int row = skuMapper.ascCount(id, num);
}
}
在 SkuMapper 中使用 SQL 语句行级锁:
@Update("update tb_sku set num=num+#{num} where id=#{id}")
int ascCount(@Param(value = "id") Long id, @Param(value = "num")Integer num);
提供 feign 调用:
关闭订单用微信支付开发文档中提供的 ”关闭订单“ API。
之前都是使用 HttpClient 工具,这次试着直接用 sdk,可以看到,在 WxPay 中,有相应方法:
可以看到,需要传 reqData ,即 向 wxpay post 的数据(和接口文档中是对应的:
必须有 appid、mch_id、out_trade_no、nonce_str、sign 参数,WeiXinPayServiceImpl 类有从配置文件中读取参数作为属性,所以可以在 WeiXinPayServiceImpl 类中提供方法,把这些参数传进去)。
/**
* 构建 WXPay 对象
*/
public WXPay wxPay(){
return new WXPay(new WXPayConfig() {
@Override
public String getAppID() {
return getAppid();
}
@Override
public String getMchID() {
return getMchID();
}
@Override
public String getKey() {
return getPartnerkey();
}
@Override
public InputStream getCertStream() {
return null;
}
@Override
public int getHttpConnectTimeoutMs() {
return 8000;
}
@Override
public int getHttpReadTimeoutMs() {
return 10000;
}
});
}
@Override
public Map cancelOrder(String outTradeNo) throws Exception {
// 封装参数
Map<String,String> reqDate=new HashMap<>();
reqDate.put("out_trade_no", outTradeNo);
return wxPay().closeOrder(reqDate);
}
可以看到,参数齐活了(不需要手动传 nonce_str、sign 是因为在 closeOrder 中,会调用 fillRequestData 方法,使用了 WXPayUtil.generateNonceStr() 作为 “nonce_str” 的值、 使用了 WXPayUtil.generateSignature(…)作为 “sign” 的值)。
需要在 WeiXinPayController 中再提供一个 取消订单的方法以供调用:
@RequestMapping(value = "/cancel/order")
public Result cancelOrder(@RequestParam String outTradeNo) throws Exception {
Map resultMap = weixinPayService.cancelOrder(outTradeNo);
return new Result(true, StatusCode.OK, "取消订单成功", resultMap);
}
先测试取消订单的方法:
返回这样的结果是因为订单已支付:
根据这个错误码,我们知道,关闭订单也是有可能出现错误信息的,不是全部的订单都可以成关闭,所以修改 WeiXinPayController 层的方法:
想让监听支付结果的 getMessage 方法里调用取消订单的方法,可以使用 Feign 调用:
@FeignClient("weixinpay")
@RequestMapping(value = "/weixin/pay")
public interface WeiXinPayFeign {
/**
* 取消订单
* @param outTradeNo
* @return
*/
@RequestMapping(value = "/cancel/order")
public Result cancelOrder(@RequestParam String outTradeNo);
}
同样地,在 (七) 超时处理中,需要把逻辑改为,监听队列的 DelayMessageListener 的 delayMessage 方法,在监听到消息时,需要判断订单是否支付成功,如果没有支付成功,需要关闭订单、回滚库存。
进行测试,然后发现 order 和 pay 工程,循环依赖了😓😓😓。
问题是这样的,order 工程中,监听订单的 DelayMessageListener 类用到了 WeiXinPayFeign :
需要在 order 工程中去掉 changgou-service-pay 的依赖,换成 changgou-service-weixinpay-api 依赖,并在启动类上声明。(所以说,如果没有导入 feign 所在工程的依赖,而且启动类上也没声明 feign 所在包的话,导入 feign 调用的路径对应的代码的工程依赖就行啦???)
这样 order 工程就没什么问题了。
现在不循环依赖了,试着启动 order 工程,是没有问题的,再来启动 pay 工程,报了个错:
主要是在 pay 工程里,用了 order 的服务层:
就是这个关闭订单的方法里,用到了逻辑删除订单的方法,会设置订单状态为支付失败,并回滚库存。
这里,pay 的 controller 层里调用了 order 的服务层,又没有专门配置,所以报错了,解决过程见 https://blog.csdn.net/weixin_41750142/article/details/116277095。
试着启动 pay 工程,报错:
它说… … order 工程里的 CartServiceImpl 里 用到了 SkuFeign,但是找不到这样的 feign。奇了怪了… … order 工程运行起来都没有报错,为什么在 pay 工程里,并没有用到 CartService,反而报错了呢?(破案了,因为在启动类上写了 @ComponentScan(basePackages = {"com.changgou.order.service"})
,这样就会扫描到这个包)
还有 pay 的 controller 层里调用了 order 的服务层,毕竟也是不合理的,如果业务交叉了,可以考虑调用 feign,但是现在又没有现成的控制层逻辑可以用来进行逻辑删除,因为 orderService.deleteOrder 方法,是我直接写在服务层的… …本来就没打算让控制层调 😰。
整个过程是这样的。在 order 工程中,一旦监听队列变化的方法,支付结果为支付失败时,就会 /cancel/order 的 feign,这个 feign 对应的是 pay 工程里的 cancelOrder 方法(为什么要放在 pay 工程里呢?是因为调用了微信支付提供的 “关闭订单” API ,在关闭订单后,才改变我们数据库里的订单状态,和回滚库存的),然后这个 pay 工程中的 cancelOrder 方法,改变订单状态和回滚库存,用的是 order 的服务层了。绕来绕去,order 用 feign 调 pay,然后 pay 的控制层 调 order 的服务层,结果又是 order 的另一个服务层报错了 🥶 。
所以想要改 bug 很简单,不要在 pay 的控制层 调 order 的服务层!! 监听到队列的支付结果是支付失败时,调 /cancel/order 的 feign,但是 cancelOrder 方法里不要 改变订单状态和回滚库存,而是继续在监听队列的方法里,改变订单状态和回滚库存,这样就是 order 工程里调 order 的代码了。
原先:
@RequestMapping(value = "/cancel/order")
public Result cancelOrder(@RequestParam String outTradeNo) throws Exception {
Map resultMap = weixinPayService.cancelOrder(outTradeNo);
if (resultMap.get("result_code").equals("FAIL")) {
// 取消订单失败
return new Result(true, StatusCode.ERROR, "取消订单失败", resultMap);
} else {
// 回滚库存
orderService.deleteOrder(outTradeNo);
return new Result(true, StatusCode.OK, "取消订单成功", resultMap);
}
}
这个方法是测试过的,没有问题。
修改为:
这样,pay 的 控制层不再调 order 的服务层了。
修改监听队列的方法:
这样 pay 工程里也就不需要 order 依赖了,否则还牵扯认证的问题。
补充:发现原先的逻辑有误,原先的代码:
@Component
@RabbitListener(queues = "${mq.pay.queue.order}")
public class OrderMessageListener {
@Autowired
OrderService orderService;
@Autowired
WeiXinPayFeign weiXinPayFeign;
/**
* 支付结果监听
*/
@RabbitHandler
public void getMessage(String message) throws Exception {
// 支付结果
Map<String, String> resultMap = JSON.parseObject(message, Map.class);
// 输出监听到的消息
System.out.println(resultMap);
// 通信标识
String returnCode = resultMap.get("return_code");
if (returnCode.equals("SUCCESS")) {
// 业务结果
String resultCode = resultMap.get("result_code");
// 订单号
String outTradeNo = resultMap.get("out_trade_no");
// 支付成功
if (resultCode.equals("SUCCESS")) {
// 更改订单信息
orderService.updateStatus(outTradeNo, resultMap.get("time_end"), resultMap.get("transaction_id"));
} else {
// 如果支付失败,需要关闭支付,取消订单,回滚库存
weiXinPayFeign.cancelOrder(outTradeNo);
}
}
}
}
这还分支付成功和支付失败的情况呢是🥶🥶🥶,实际上,只有支付成功的时候才会进行结果通知回调的,支付失败的话根本就不会到这个方法里来,所以,需要修改为:
@RabbitHandler
public void getMessage(String message) throws Exception {
// 支付结果
Map<String, String> resultMap = JSON.parseObject(message, Map.class);
// 输出监听到的消息
System.out.println(resultMap);
// 订单号
String outTradeNo = resultMap.get("out_trade_no");
// 更改订单信息
orderService.updateStatus(outTradeNo, resultMap.get("time_end"), resultMap.get("transaction_id"));
}
再有,原先的代码认为当 returnCode、resultCode 都为 SUCCESS 时,表示支付成功,需要修改支付状态为“已支付”,否则表示支付失败。然而,未支付的时候,returnCode、resultCode 也是为 SUCCESS 的: (returnCode、resultCode 只是说明可以正常返回响应结果,但不是说明支付成功)
所以如果要分支付成功和支付失败的情况的话,应该以交易状态作为判断依据:
// 交易状态
String tradeState=resultMap.get("trade_state");
// 订单号
String outTradeNo = resultMap.get("out_trade_no");
if (tradeState.equals("SUCCESS")) {
// 支付成功
// 更改订单信息
orderService.updateStatus(outTradeNo, resultMap.get("time_end"), resultMap.get("transaction_id"));
} else {
// 如果支付失败,需要关闭支付,取消订单,回滚库存
weiXinPayFeign.cancelOrder(outTradeNo);
}
九、总结
(1)导入微信支付的依赖,就可以使用 wxPayUtil 类 的一些方法,比如 获取随机字符串、Map 和 XML 的互相转换、通过密钥生成签名。
(2)使用封装了 HttpClient 的工具类访问微信支付服务器(其实直接用 WeiXinPay 类里的方法也行🤗)。
(3)调用 “统一下单" API,提供 支付订单号 out_trade_no 和 支付总金额 total_fee 参数,会得到用于支付扫描的二维码链接。
(4)调用 ”查询订单“ API,通过商品订单号进行查询。因为可能存在 像网络原因,导致我们本地服务器没有及时拿到,所以需要手动查询。
(5)至此,都是本地服务访问微信支付服务器,接下来,需要让微信支付服务器【外网】访问本地,使用内网穿透技术 NAT。提供外网域名、内网主机(IP地址)、内网端口,即可生成一个访问地址,通过这个访问地址,就可以让外网访问本地的服务了。
(6)在 pay 工程中提供获取支付结果的方法,同时,还应该让 order 工程也获取到,思路是使用消息队列,监听消息队列,当监听队列有支付成功的消息时,把订单状态改为支付成功。
创建 名为 queue.order 队列,和 名为 exchange.order 的交换机,并进行绑定。
交换机是生产者和队列之间的抽象,使得生产者与队列无直接联系,用于指定消息按什么规则,路由到哪个队列。生产者通过路由键 routingkey 将交换机和队列绑定起来。
获取支付结果,调用 “支付结果通知” API,需要先将对应的路径(内网穿透地址)作为配置文件中 weixin:notifyurl 的属性值。这样,用户扫描付款码支付成功后,会调用到这个方法(这·就是回调?)并把支付结果发送给 queue.order 队列。
OrderMessageListener 类实现对 queue.order 队列 的监听,通过消息的 “result_code” 参数判断是否支付成功。如果支付成功,需要修改订单状态,逻辑是 先通过商品订单号 查询到订单信息,设置订单支付时间为当前时间,设置支付状态为 “1” 已支付,设置事务 ID 为”支付结果通知“里的事务 ID,并把订单对应的记录更新到数据库 order 表中;如果支付失败,需要关闭订单,调用 ”关闭订单“ API,如果返回的结果中,result_code 为 SUCCESS,说明关闭订单成功,还需要回滚库存(因为之前下单的时候减少了库存),否则,说明可能遇到了 订单已支付、订单已关闭、系统错误等错误,只是将错误码放在响应结果中返回,并不会回滚库存。
(7)如果用户下单 30 分钟后还没有支付,也需要把订单取消。使用 Rabbit MQ 的 Time To Live、Dead Letter Exchange 属性,队列 orderDelayQueue 一过期,就把消息转发给队列 orderListenerQueue。在下单的 addOrder 方法中,添加逻辑:将订单消息发送给 orderDelayQueue 队列,并设置过期时间。DelayMessageListener 类实现对 orderListenerQueue 队列的监听。
更多推荐
所有评论(0)