好好學習,天天向上
本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star
- 暢購商城(一):環境搭建
- 暢購商城(二):分布式文件系統FastDFS
- 暢購商城(三):商品管理
- 暢購商城(四):Lua、OpenResty、Canal實現廣告緩存與同步
- 暢購商城(五):Elasticsearch實現商品搜索
- 暢購商城(六):商品搜索
- 暢購商城(七):Thymeleaf實現靜態頁
- 暢購商城(八):微服務網關和JWT令牌
- 暢購商城(九):Spring Security Oauth2
- 暢購商城(十):購物車
- 暢購商城(十一):訂單
- 暢購商城(十二):接入微信支付
- 暢購商城(十三):秒殺系統「上」
- 暢購商城(十四):秒殺系統「下」
支付流程
為了實現支付的功能,這里選擇接入微信支付。流程就是我們通過訂單系統下單,然后訂單系統調用支付系統去向微信支付的服務器發送請求,然后獲取二維碼返回給用戶,然后訂單系統就開始監聽MQ。用戶掃碼支付后,支付系統將支付狀態存進MQ中。訂單系統檢測到用戶已經付錢了,就將訂單設為已支付,然后存進MySQL中。可能會因為網絡問題導致訂單系統獲取不到支付狀態,所以訂單系統會定時向微信支付服務器發送請求去查詢訂單狀態。
微信支付簡介
要想接入微信支付,就得有認證過的服務號,這個我沒有,所以申請不了。就用黑馬提供的賬號吧,我試了一下,可以用。
appid(公眾賬號ID):wx8397f8696b538317
mch_id(商戶號):1473426802
key(商戶密鑰):T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
微信支付的開發文檔:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1
在這個官方的開發文檔里面介紹了微信支付相關的API並且提供了SDK。
這個SDK的內容不多,只有幾個類,我稍微介紹一下:
-
WXPayConfig:這是個抽象類,里面有幾個方法,是用來獲取核心參數的,比如公眾號id,商戶號,密鑰等,所以在使用的時候要先去實例化這個類將幾個重要參數配置進去。
-
WXPay:和訂單相關的方法都封裝在這個類里面,比如下單,查詢訂單,取消訂單等。里面有個方法fillRequestData(),每次執行下單等操作的時候都會去調用這個方法將WXPayConfig中的幾個核心參數封裝到請求參數的Map集合里。
-
WXPayRequest:這個是負責請求服務器的,WXPay也都是通過調用這個類中的方法去請求服務器的,執行相應方法的時候,會將WXPay傳過來的Map集合轉換成XML格式的字符串,然后使用HttpClient向服務器發送請求。沒錯,微信支付是通過XML進行數據傳輸的。
-
WXPayUtil:這是個工具類,封裝了一些常用方法,比如Map轉XML,XML轉Map等。
在這個項目中用到的微信的Native支付,也就是掃碼支付,有兩種模式,我們用到的是模式二。
首先在暢購的訂單系統中生成訂單,然后將一些必要的參數傳入到微信支付的后台,然后就會產生一個預支付的訂單,將支付鏈接返回給我們,我們再根據支付鏈接生成二維碼傳給用戶。用戶掃碼支付后再將支付結果傳到我們的后台,這樣整個支付的流程就結束了。
准備工作
介紹完了微信支付后,就來說一下項目中該怎么去集成微信支付。視頻中用的是第三方的依賴,我用的是官方的。微信支付的SDK在Maven的遠程倉庫里是沒有的,所以需要自己下載然后手動導入。這里面有兩個坑有必要說一下,前面不是提到WXPayConfig是個抽象類么,那么用的時候肯定得去繼承才能實例化吧。但是里面的抽象方法都沒有權限修飾符,所以默認是包訪問權限,我們既然是Maven依賴這個SDK,那么我們寫的代碼自然不會和它在同一個包下,所以要先在這幾個抽象方法前面添加public修飾符。而且,微信提供的sdk文檔里還寫成了implements抽象類,真搞不懂微信怎么會犯這種錯~~~
現在就可以將這個SDK添加到我們本地的Maven倉庫里了,在解壓后的sdk的根目錄下執行mvn install
命令。
當出現BUILD SUCCESS的字樣的時候,就說明已經成功添加到本地的Maven倉庫了。這時候第二個坑就來了,如果就這么添加到我們的項目中就有可能會出現Maven依賴沖突:
可以看到,出現沖突的包是slf4j-simple,而微信支付sdk恰好依賴了這個包,所以在導入微信支付的時候把這個依賴排除掉即可。
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>3.0.9</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
然后就可以創建支付的工程了。在changgou-service下創建一個Moudle名為changgou-service-wechatpay,然后將微信支付的依賴添加到這個工程下,啟動類沒啥好說的,配置文件如下:
server:
port: 18090
spring:
application:
name: wechatpay
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
#微信支付信息配置
wechat:
# 應用id
app_id: wx8397f8696b538317
# 商戶號id
mch_id: 1473426802
# 密鑰
key: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
# 支付回調地址
notify_url: http://www.itcast.cn
這里配置了appid,mch_id,key,notify_url這幾個參數,用的時候直接讀取就可以了。這樣支付微服務就搭建好了。
微信支付二維碼生成
添加一個Controller層的類WeChatPayController,然后再創建一個入口方法,因為二維碼支付叫做Native支付,所以這里就起名為createNative了,接收一個Order參數,視頻中用的是Map接收參數。但是既然是創建訂單,直接用Order作為參數可讀性不是好很多嗎。
//創建二維碼
@RequestMapping(value = "/create/native")
public Result createNative(@RequestBody Order order){
Map<String,String> resultMap = weChatPayService.createNative(order);
return new Result(true, StatusCode.OK,"創建二維碼預付訂單成功!",resultMap);
}
寫完了Controller就可以寫Service層了,視頻中是用HttpClient調用微信支付的遠程接口,既然微信已經提供了sdk,為什么還要重復造輪子呢?所以我就沒和視頻中寫的一樣,而是直接使用微信的sdk,雖然底層用的也是HttpClient。
首先需要將WXPayConfig給實現一下。
public class MyWXPayConfig extends WXPayConfig {
private String appId;
private String mchId;
private String key;
public MyWXPayConfig(String appId, String mchId, String key) {
this.appId = appId;
this.mchId = mchId;
this.key = key;
}
@Override
public String getAppID() {
return appId;
}
@Override
public String getMchID() {
return mchId;
}
@Override
public String getKey() {
return key;
}
@Override
public InputStream getCertStream() {
return null;
}
@Override
public int getHttpConnectTimeoutMs() {
return 8000;
}
@Override
public int getHttpReadTimeoutMs() {
return 10000;
}
@Override
public IWXPayDomain getWXPayDomain() {
IWXPayDomain iwxPayDomain = new IWXPayDomain() {
@Override
public void report(String domain, long elapsedTimeMillis, Exception ex) {
}
@Override
public DomainInfo getDomain(WXPayConfig config) {
return new IWXPayDomain.DomainInfo(WXPayConstants.DOMAIN_API, true);
}
};
return iwxPayDomain;
}
}
然后在WeChatPayServiceImpl編寫相應的代碼。
@Value("${wechat.appid}")
private String appId;
@Value("${wechat.mch_id}")
private String mcnId;
@Value("${wechat.key}")
private String key;
@Value("${wechat.notify_url}")
private String notifyUrl;
@Override
public Map<String, String> createNative(Order order) {
try {
Map<String, String> map = new HashMap<>(16);
map.put("body", "騰訊充值中心-QQ會員充值"); //商品描述
map.put("out_trade_no", order.getId()); //商戶訂單號
map.put("total_fee", String.valueOf((int)(order.getTotalMoney() * 100))); //標價金額,單位為分
map.put("spbill_create_ip", "127.0.0.1"); //終端IP
map.put("trade_type", "NATIVE "); //交易類型,JSAPI -JSAPI支付,NATIVE -Native支付,APP -APP支付
Map<String, String> response = wxpay.unifiedOrder(map);
if (response == null || response.size() == 0) {
throw new RuntimeException("下單失敗");
}
return response;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
這里用到了@Value注解將配置文件中的幾個參數注入了進來。然后將創建訂單的幾個參數添加到Map集合中,鍵的名稱在文檔中有說明,不能改變,金額的單位是分,我們傳過來的是元,所以需要乘100,微信要求金額是整數,所以再強轉成int。然后就去創建MyWXPayConfig的實例,將三個參數傳入進入,然后去創建WXPay的對象,將config和notifyUrl傳入進去,然后調用unifiedOrder()方法把map傳入進去就可以創建訂單了,其實內部就是把appid這幾個參數放在了我們傳入的map中,所以在這里放入到map中也是OK的。
測試一下,OK了,成功創建了訂單,也拿到了二維碼的地址,接下來只要把code_url轉換成二維碼圖片就可以了。
如果是做測試,隨便在網上找個二維碼生成器,然后把code_url復制過去就行了。在項目中是用qrious生成。
<html>
<head>
<title>二維碼入門小demo</title>
<!--1.引入js 2. 創建一個img標簽 用來存儲顯示二維碼的圖片 3.創建js對象 4.設置js對象的配置項-->
<script src="qrious.js"> </script> <!--下載qrious.js后引入-->
</head>
<body>
<img id="myqrious" >
</body>
<script>
var qrious = new QRious({
element:document.getElementById("myqrious"),// 指定的是圖片所在的DOM對象
size:250,//指定圖片的像素大小
level:'H',//指定二維碼的容錯級別(H:可以恢復30%的數據)
value:'weixin://wxpay/bizpayurl?pr=xKlU7lD'//指定二維碼圖片代表的真正的值
})
</script>
</html>
查詢訂單狀態
有時候可能因為網絡原因導致支付狀態沒有及時返回到我們的服務器中,這個時候就要手動地去查詢了,代碼很簡單,我就不多說了。
//WeChatPayController
@GetMapping(value = "/status/query")
public Result<Map<String,String>> queryPayStatus(@RequestParam String outTradeNo) {
Map<String,String> resultMap = weChatPayService.queryPayStatus(outTradeNo);
return new Result<>(true, StatusCode.OK,"訂單查詢成功",resultMap);
}
--------------------------------------------------------------------------------------
//WeChatPayServiceImpl
@Override
public Map<String, String> queryPayStatus(String outTradeNo) {
try {
Map<String, String> map = new HashMap<>();
map.put("out_trade_no", outTradeNo); //商戶訂單號
MyWXPayConfig config = new MyWXPayConfig(appId,mcnId, key);
WXPay wxpay = new WXPay(config,notifyUrl);
Map<String, String> response = wxpay.orderQuery(map);
if (response == null || response.size() == 0) {
throw new RuntimeException("訂單查詢失敗");
}
return response;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
測試一下:
支付結果回調通知及RabbitMQ監聽
要實現支付結果回調通知,首先得告訴微信支付的服務器我們的支付結果回調通知的地址,因為我們沒有公網IP,所以微信服務器沒辦法主動發消息到我們的服務器,所以可以使用內網穿透。我選擇的是uTools的一個內網穿透插件,不用注冊賬號就可以直接使用,很方便。
然后將內網穿透的地址配置到之前配置notify_url的地方就可以了。
# 支付回調地址
notify_url: http://robod123.cn1.utools.club/wechat/pay/notify/url
接下來就該配置RabbitMQ了,首先得在虛擬機中安裝RabbitMQ:
docker pull docker.io/rabbitmq:3.7-management # 下載rabbitmq的鏡像
docker run --name rabbitmq -d -p 15672:15672 -p 5672:5672 4b23cfb64730 # 安裝rabbitmq,最后面的id是docker images查出來的
docker exec -it rabbitmq /bin/bash # 進入到rabbitmq中
rabbitmqctl add_user root 123456 # 添加一個名為root的用戶,密碼為123456
rabbitmqctl set_permissions -p / root ".*" ".*" ".*" # 賦予root用戶所有權限
rabbitmqctl set_user_tags root administrator # 賦予root用戶administrator角色
因為支付微服務和訂單微服務都用到了RabbitMQ,所以在這兩個微服務中添加mq的依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
然后在這兩個微服務中添加rabbitmq的配置信息:
spring: # 配置rabbitmq的ip和端口
rabbitmq:
host: 192.168.31.200
port: 5672
username: root
password: 123456
接着就去添加exchange和queue了,視頻中是用程序自動創建的,但是我遇到了一個問題,就是訂單微服務啟動的時候報了一個錯,導致服務啟動不了:
org.springframework.amqp.rabbit.listener.QueuesNotAvailableException: Cannot prepare queue for listener. Either the queue doesn't exist or the broker will not allow us to use it.
所以我就在網頁端手動創建exchange和queue了,並指定routing key將exchange和queue綁定起來。
配置的部分到這里就已經完成了,接下來就可以去寫代碼了,為了方便理解,我畫了一張流程圖:
代碼我就不去解釋了,直接貼了,對照着這張流程圖應該就能看懂了。視頻中提到的兩個小作業我也寫了。
WeChatPayController
:
/**
* 支付結果回調通知
* @param request
* @return
* @throws Exception
*/
@RequestMapping("/notify/url")
public String notifyUrl(HttpServletRequest request) throws Exception {
ServletInputStream inputStream = request.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inputStream.read(buffer))!=-1) {
outputStream.write(buffer,0,len);
}
String xmlString = outputStream.toString("UTF-8");
//將java對象轉換成amqp消息發送出去,調用的是send方法
rabbitTemplate.convertAndSend("exchange.order","routing.order", xmlString);
return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
}
OrderMessageListener
@Component
@RabbitListener(queues = {"queue.order"})
public class OrderMessageListener {
private final static String SUCCESS = "SUCCESS";
@Autowired
private OrderService orderService;
@Autowired
private WeChatPayFeign weChatPayFeign;
/**
* 監聽mq的消息
* @param message xml格式的消息
*/
@RabbitHandler
public void getMessage(String message) throws Exception {
Map<String, String> map = WXPayUtil.xmlToMap(message);
String returnCode = map.getOrDefault("return_code",""); //返回狀態碼
String resultCode = map.getOrDefault("result_code",""); //業務結果
if (SUCCESS.equals(returnCode)) { //支付成功,修改訂單狀態
String outTradeNo = map.get("out_trade_no"); //商戶訂單號
if (! SUCCESS.equals(resultCode)) { //交易失敗,關閉訂單,從數據庫中將訂單狀態修改為支付失敗,回滾庫存
Map<String, String> closeResult = weChatPayFeign.closeOrder(outTradeNo).getData(); //關閉訂單時服務器返回的數據
//如果錯誤代碼為ORDERPAID則說明訂單已經支付,當作正常訂單處理,反之 回滾庫存
if (!("FAIL".equals(closeResult.get("result_code")) && "ORDERPAID".equals(closeResult.get("err_code")))) {
orderService.deleteOrder(outTradeNo);
return;
}
}
String transactionId = map.get("transaction_id"); //微信支付訂單號
String timeEnd = map.get("time_end"); //支付完成時間
orderService.updateStatus(outTradeNo,timeEnd,transactionId);
}
}
}
OrderServiceImpl
@Override
public void updateStatus(String outTradeNo,String timeEnd,String transactionId) {
Order order = orderMapper.findById(outTradeNo);
LocalDateTime payTime = LocalDateTime.parse(timeEnd, DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
order.setPayStatus("1"); //支付狀態修改為1表示已支付
order.setTransactionId(transactionId); //交易流水號
order.setPayTime(payTime); //交易時間
orderMapper.updateByPrimaryKey(order);
}
@Override
public void deleteOrder(String outTradeNo) {
Order order = orderMapper.findById(outTradeNo);
LocalDateTime time = LocalDateTime.now();
//修改狀態
order.setPayStatus("2"); //交易失敗
order.setUpdateTime(time);
//提交到數據庫
orderMapper.updateByPrimaryKey(order);
//回滾庫存
List<OrderItem> orderItems = orderItemMapper.findByOrderId(order.getId());
List<Long> skuIds = new ArrayList<>();
for (OrderItem orderItem : orderItems) {
skuIds.add(orderItem.getSkuId());
}
List<Sku> skuList = skuFeign.findBySkuIds(order.getSkuIds()).getData(); //數據庫中對應的sku集合
Map<Long, Sku> skuMap = skuList.stream().collect(Collectors.toMap(Sku::getId, a -> a));
for (OrderItem orderItem : orderItems) {
Sku sku = skuMap.get(orderItem.getSkuId());
sku.setNum(sku.getNum()+orderItem.getNum()); //加庫存
}
skuFeign.updateMap(skuMap);
}
定時處理訂單狀態
當創建一個訂單后,可能用戶並不會去支付,或者是因為網絡的原因導致我們獲取不到用戶支付的結果。所以可以采用定時處理的方式,具體的操作就是下單成功后就往mq的隊列1中發送一條消息,設置30分鍾過期,過期后將消息發送給隊列2,然后我們監聽隊列2。這樣一旦監聽到了隊列2的消息,則說明離下單已經過去了30分鍾,這時候我們去查詢一下訂單狀態,如果是已支付就不去管它,要是未支付的話就通知微信服務器關閉訂單,然后從數據庫中刪除訂單。為了方便理解,我畫了一張流程圖:
代碼挺簡單的,首先將添加一個配置類去把隊列創建出來,直接在web頁面上配置也是OK的,不過上一節是在web頁面配置的,這個就在程序中添加吧。
@Configuration
public class MqConfig {
//隊列1,延時隊列,消息過期后發送給隊列2
@Bean
public Queue orderDelayQueue() {
return QueueBuilder
.durable("orderDelayQueue")
//orderDelayQueue隊列信息會過期,過期之后,進入到死信隊列,死信隊列數據綁定到其他交換機
.withArgument("x-dead-letter-exchange","orderListenerExchange")
.withArgument("x-dead-letter-routing-key","orderListenerRoutingKey") //綁定指定的routing-key
.build();
}
//隊列2
@Bean(name = "orderListenerQueue") //名稱不寫默認就是方法名
public Queue orderListenerQueue() {
return new Queue("orderListenerQueue",true);
}
//創建交換機
@Bean
public Exchange orderListenerExchange() {
return new DirectExchange("orderListenerExchange");
}
//隊列queue2綁定exchange
@Bean
public Binding orderListenerBinding(Queue orderListenerQueue,Exchange orderListenerExchange) {
return BindingBuilder
.bind(orderListenerQueue)
.to(orderListenerExchange)
.with("orderListenerRoutingKey")
.noargs();
}
}
在創建隊列1的時候,配置了將死信隊列的數據綁定到隊列2的交換機上,這樣數據過期后就會被發送到隊列2中。
然后在創建訂單的時候將訂單號發送到隊列1中:
//OrderServiceImpl
@Override
public synchronized void add(Order order) {
order.setId(String.valueOf(idWorker.nextId()));
…………
rabbitTemplate.convertAndSend("orderDelayQueue", (Object)order.getId(), new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("30*60*1000"); //定時30min后過時
return message;
}
});
}
監聽隊列2然后執行相應的操作
@Component
@RabbitListener(queues = "orderListenerQueue")
public class DelayMessageQueueListener {
@Autowired
private OrderService orderService;
@Autowired
private WeChatPayFeign weChatPayFeign;
@RabbitHandler
public void getMessage(String orderId) throws Exception {
Order order = orderService.findById(orderId);
if ("0".equals(order.getPayStatus())) {
//0表示未支付,通知微信服務器取消訂單,從數據庫中刪除訂單,回滾庫存
Map<String, String> closeResult = weChatPayFeign.closeOrder(orderId).getData();
//如果錯誤代碼為ORDERPAID則說明訂單已經支付,當作正常訂單處理,反之 回滾庫存
if (!("FAIL".equals(closeResult.get("result_code")) && "ORDERPAID".equals(closeResult.get("err_code")))) {
orderService.deleteOrder(orderId);
}
}
}
}
OK,這樣就可以定時去處理訂單數據了。
總結
這篇文章主要就是實現了微信掃碼支付,因為微信提供的sdk有點問題,還是花了一些時間的,不過最后還是完成了。然后又實現了支付結果回調通知和rabbitmq監聽的功能,用到了內網穿透。最后使用兩個隊列來實現了定時處理訂單。還有一個問題就是我沒系統地學過RabbitMQ,所以對於很多操作都是一知半解的,都是參考着視頻中講的然后再自己摸索摸索web頁面的配置,后期我會系統地學習一下RabbitMQ然后再總結出一篇文章。
如果覺得我寫的還可以,請不要吝嗇你的
贊
贊
贊
!代碼:https://github.com/RobodLee/changgou
本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star