尚硅谷2020微服务分布式电商项目《谷粒商城》-支付、秒杀
學習更多的知識,整理不易,拒絕白嫖,記得三連哦
關注公眾號:java星星 獲取全套課件資料
1. 支付
訂單搞定之后就是支付了,首先搭建支付工程。
1.1. 搭建環境
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.atguigu</groupId><artifactId>gmall-1010</artifactId><version>0.0.1-SNAPSHOT</version></parent><groupId>com.atguigu</groupId><artifactId>gmall-payment</artifactId><version>0.0.1-SNAPSHOT</version><name>gmall-payment</name><description>谷粒商城支付系統</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>com.atguigu</groupId><artifactId>gmall-common</artifactId><version>0.0.1-SNAPSHOT</version></dependency><dependency><groupId>com.atguigu</groupId><artifactId>gmall-oms-interface</artifactId><version>0.0.1-SNAPSHOT</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-zipkin</artifactId></dependency><dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>4.10.0.ALL</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>bootstrap.yml:
spring:application:name: payment-servicecloud:nacos:config:server-addr: 127.0.0.1:8848application.yml:
server:port: 18092 spring:cloud:nacos:discovery:server-addr: localhost:8848sentinel:transport:dashboard: localhost:8080port: 8179zipkin:base-url: http://localhost:9411/sender:type: webdiscovery-client-enabled: falsesleuth:sampler:probability: 1redis:host: 172.16.116.100rabbitmq:host: 172.16.116.100virtual-host: /fenggeusername: fenggepassword: fenggelistener:simple:acknowledge-mode: manualprefetch: 1thymeleaf:cache: falsedatasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://172.16.116.100:3306/guli_paymentusername: rootpassword: root feign:sentinel:enabled: true mybatis-plus:global-config:db-config:id-type: auto啟動類:
@SpringBootApplication @EnableFeignClients @MapperScan("com.atguigu.gmall.payment.mapper") public class GmallPaymentApplication {public static void main(String[] args) {SpringApplication.run(GmallPaymentApplication.class, args);}}網關配置:
nginx配置:加入payment.gmall.com
server {listen 80;server_name api.gmall.com search.gmall.com www.gmall.com item.gmall.com sso.gmall.com cart.gmall.com order.gmall.com payment.gmall.com;proxy_set_header Host $host;location / {proxy_pass http://192.168.221.1:8888;} }重新加載nginx配置:nginx -s reload
在hosts中添加payment.gmall.com:
1.2. 支付流程
支付流程如下:
點擊提交訂單按鈕,完成訂單創建后,跳轉到支付選擇頁
選擇支付渠道,點擊立即支付。跳轉到具體的支付頁
用戶掃碼支付跳轉到支付成功頁
1.3. 選擇支付方式
下單成功后,請求路徑:http://payment.gmall.com/pay.html?orderToken=202006041844036401268493714385424386
已知條件是訂單編號,而支付可能需要訂單金額等一些訂單信息。所以訂單工程應該提供一個根據訂單編號查詢訂單的數據接口。
1.3.1. 根據訂單編號查詢訂單
在gmall-oms工程中的OrderController中添加根據訂單編號查詢訂單的接口方法:
@GetMapping("token/{orderSn}") public ResponseVo<OrderEntity> queryOrderByOrderSn(@PathVariable("orderSn")String orderSn){OrderEntity orderEntity = this.orderService.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));return ResponseVo.ok(orderEntity); }在gmall-oms-interface工程中的GmallOmsApi添加接口方法:
@GetMapping("oms/order/token/{orderSn}") public ResponseVo<OrderEntity> queryOrderByOrderSn(@PathVariable("orderSn")String orderSn);1.3.2. 跳轉到支付渠道選擇頁
在gmall-payment工程中實現頁面跳轉。
PaymentController:
@Controller public class PaymentController {@Autowiredprivate PaymentService paymentService;@GetMapping("pay.html")public String toPay(@RequestParam("orderToken") String orderToken, Model model){OrderEntity orderEntity = this.paymentService.queryOrderByOrderToken(orderToken);model.addAttribute("orderEntity", orderEntity);return "pay";} }PaymentService:
@Service public class PaymentService {@Autowiredprivate GmallOmsClient omsClient;public OrderEntity queryOrderByOrderToken(String orderToken) {ResponseVo<OrderEntity> orderEntityResponseVo = this.omsClient.queryOrderByOrderSn(orderToken);return orderEntityResponseVo.getData();} }GmallOmsClient:
@FeignClient("oms-service") public interface GmallOmsClient extends GmallOmsApi { }1.3.3. 頁面渲染
1.4. 完成支付功能
這里支付已支付寶為例,支付寶的支付流程如下:
調用順序如下:
注意:
- 由于同步返回的不可靠性,支付結果必須以異步通知或查詢接口返回為準,不能依賴同步跳轉。
- 商戶系統接收到異步通知以后,必須通過驗簽(驗證通知中的 sign 參數)來確保支付通知是由支付寶發送的。詳細驗簽規則參考異步通知驗簽。
- 接收到異步通知并驗簽通過后,一定要檢查通知內容,包括通知中的 app_id、out_trade_no、total_amount 是否與請求中的一致,并根據 trade_status 進行后續業務處理。
- 在支付寶端,partnerId 與 out_trade_no 唯一對應一筆單據,商戶端保證不同次支付 out_trade_no 不可重復;若重復,支付寶會關聯到原單據,基本信息一致的情況下會以原單據為準進行支付。
1.4.1. 內網穿透
支付異步通知需要獨立ip使阿里支付成功后可以回調我們的接口,所以前提條件就是內網穿透。
哲西云:https://cloud.zhexi.tech
哲西云瀏覽器客戶端配置隧道,映射網關的8888端口:
具體配置如下:
測試內網穿透:訪問品牌列表
使用內網穿透后,外網無法通過payment.gmall.com訪問支付系統了。只能通過內網穿透提供的地址訪問,那么我們的網關也就無法通過域名轉發請求,只能通過路徑轉發,于是在網關中配置路徑路由:
1.4.2. 表、實體類及Mapper接口
將支付數據保存到數據庫,以便跟支付寶進行對賬。
創建guli_payment數據庫,導入一下sql:
CREATE TABLE `payment_info` (`id` bigint(20) NOT NULL COMMENT '商戶訂單號',`out_trade_no` varchar(64) DEFAULT NULL,`payment_type` tinyint(4) DEFAULT NULL COMMENT '支付類型(微信與支付寶)',`trade_no` varchar(64) DEFAULT NULL COMMENT '支付寶交易憑證號',`total_amount` decimal(18,4) DEFAULT NULL COMMENT '訂單金額。訂單中獲取',`subject` varchar(100) DEFAULT NULL COMMENT '交易內容。利用商品名稱拼接。',`payment_status` tinyint(4) DEFAULT NULL COMMENT '支付狀態,默認值0-未支付,1-已支付。',`create_time` datetime DEFAULT NULL COMMENT '創建時間',`callback_time` datetime DEFAULT NULL COMMENT '回調時間,初始為空,支付寶異步回調時記錄',`callback_content` text COMMENT '回調信息,初始為空,支付寶異步回調時記錄',PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='支付對賬表';對應的實體類如下:
@Data @TableName("payment_info") public class PaymentInfoEntity {@Idprivate Long id;private String outTradeNo;private Integer paymentType;private String tradeNo;private BigDecimal totalAmount;private String subject;private Integer paymentStatus;private Date createTime;private Date callbackTime;private String callbackContent; }mapper接口:
public interface PaymentInfoMapper extends BaseMapper<PaymentInfoEntity> { }項目結構:
1.4.3. 整合阿里支付
繼續改造gmall-order工程
在pom.xml中,引入阿里支付的依賴:
<dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>4.10.0.ALL</version> </dependency>在application.yml中添加阿里支付的配置:
alipay:app_id: 2021001163617452gatewayUrl: https://openapi.alipay.com/gateway.domerchant_private_key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQalipay_public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkWsnotify_url: http://9glldacce2.52http.net/pay/successreturn_url: http://9glldacce2.52http.net/pay/okapp_id、私鑰、公鑰參照資料中的《支付寶秘鑰.txt》
把課前資料中封裝的阿里支付工具類及PayVo對象copy到工程中:
1.4.4. 跳轉到支付
修改提交訂單的gmall-order中OrderController方法,如下:
@GetMapping("alipay.html") @ResponseBody public String alipay(@RequestParam("orderToken") String orderToken){try {// 校驗訂單狀態OrderEntity orderEntity = this.paymentService.queryOrderByOrderToken(orderToken);if (orderEntity.getStatus() != 0){throw new OrderException("此訂單無法支付,可能已經過期!");}// 調用支付寶接口獲取支付表單PayVo payVo = new PayVo();payVo.setOut_trade_no(orderEntity.getOrderSn());// payVo.setTotal_amount(orderEntity.getPayAmount().toString());payVo.setTotal_amount("0.01");payVo.setSubject("谷粒商城支付平臺");// 把支付信息保存到數據庫Long payId = this.paymentService.save(orderEntity, 1);payVo.setPassback_params(payId.toString());String form = alipayTemplate.pay(payVo);// 跳轉到支付頁return form;} catch (AlipayApiException e) {e.printStackTrace();throw new OrderException("支付出錯,請刷新后重試!");} }PaymentService:
@Service public class PaymentService {@Autowiredprivate GmallOmsClient omsClient;@Autowiredprivate PaymentInfoMapper paymentInfoMapper;public OrderEntity queryOrderByOrderToken(String orderToken) {ResponseVo<OrderEntity> orderEntityResponseVo = this.omsClient.queryOrderByOrderSn(orderToken);return orderEntityResponseVo.getData();}public Long save(OrderEntity orderEntity, Integer payType){// 查看支付記錄,是否已存在。PaymentInfoEntity paymentInfoEntity = this.paymentInfoMapper.selectOne(new QueryWrapper<PaymentInfoEntity>().eq("out_trade_no", orderEntity.getOrderSn()));// 如果存在,直接結束if (paymentInfoEntity != null) {return paymentInfoEntity.getId();}// 否則,新增支付記錄paymentInfoEntity = new PaymentInfoEntity();paymentInfoEntity.setOutTradeNo(orderEntity.getOrderSn());paymentInfoEntity.setPaymentType(payType);paymentInfoEntity.setSubject("谷粒商城支付平臺");// paymentInfoEntity.setTotalAmount(orderEntity.getPayAmount());paymentInfoEntity.setTotalAmount(new BigDecimal(0.01));paymentInfoEntity.setPaymentStatus(0);paymentInfoEntity.setCreateTime(new Date());this.paymentInfoMapper.insert(paymentInfoEntity);return paymentInfoEntity.getId();} }測試效果:
1.4.5. 異步回調
由于同步返回的不可靠性,支付結果必須以異步通知或查詢接口返回為準,不能依賴同步跳轉。
接收到回調要做的事情:
1.4.5.1. 實現異步回調方法
請求方式:Post請求
請求路徑:/pay/success
請求參數:PayAsyncVo
返回值:success/failure
給PaymentController新增支付成功后的回調方法:
@PostMapping("pay/success") @ResponseBody public String paySuccess(PayAsyncVo payAsyncVo){// 1.驗簽Boolean flag = this.alipayTemplate.verifySignature(payAsyncVo);if (!flag) {//TODO:驗簽失敗則記錄異常日志return "failure"; // 支付失敗}// 2.驗簽成功后,按照支付結果異步通知中的描述,對支付結果中的業務內容進行二次校驗String payId = payAsyncVo.getPassback_params();if (StringUtils.isBlank(payId)){return "failure";}PaymentInfoEntity paymentInfoEntity = this.paymentService.queryPayMentById(Long.valueOf(payId));if (paymentInfoEntity == null|| !StringUtils.equals(payAsyncVo.getApp_id(), this.alipayTemplate.getApp_id())|| !StringUtils.equals(payAsyncVo.getOut_trade_no(), paymentInfoEntity.getOutTradeNo())|| paymentInfoEntity.getTotalAmount().compareTo(new BigDecimal(payAsyncVo.getBuyer_pay_amount())) != 0){return "failure";}// 3.校驗支付狀態。根據 trade_status 進行后續業務處理 TRADE_SUCCESSif (!StringUtils.equals("TRADE_SUCCESS", payAsyncVo.getTrade_status())) {return "failure";}// 4.正常的支付成功,記錄支付記錄方便對賬paymentService.paySuccess(payAsyncVo);// 5.發送消息更新訂單狀態,并減庫存this.rabbitTemplate.convertAndSend("order-exchange", "order.pay", payAsyncVo.getOut_trade_no());// 6.給支付寶成功回執return "success"; }給PaymentService添加方法:
/*** 根據id查詢支付信息* @param id* @return*/ public PaymentInfoEntity queryPayMentById(Long id) {return this.paymentInfoMapper.selectById(id); }/*** 更新支付狀態* @param payAsyncVo*/ public void paySuccess(PayAsyncVo payAsyncVo){PaymentInfoEntity paymentInfoEntity = new PaymentInfoEntity();paymentInfoEntity.setCallbackTime(new Date());paymentInfoEntity.setPaymentStatus(1);paymentInfoEntity.setCallbackContent(JSON.toJSONString(payAsyncVo));this.paymentInfoMapper.update(paymentInfoEntity, new UpdateWrapper<PaymentInfoEntity>().eq("out_trade_no", payAsyncVo.getOut_trade_no())); }1.4.5.2. oms更新訂單狀態
給gmall-oms中的OrderListener添加修改訂單狀態為支付成功(待發貨)的消息監聽方法:
@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "ORDER-PAY-QUEUE", durable = "true"),exchange = @Exchange(value = "ORDER-EXCHANGE", ignoreDeclarationExceptions = "true"),key = {"order.pay"} )) public void successOrder(String orderToken, Channel channel, Message message) throws IOException {if (this.orderMapper.successOrder(orderToken) == 1){// 如果訂單支付成功,真正的減庫存this.rabbitTemplate.convertAndSend("ORDER-EXCHANGE", "stock.minus", orderToken);// 給用戶添加積分信息OrderEntity orderEntity = this.orderService.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderToken));UserBoundVO userBoundVO = new UserBoundVO();userBoundVO.setUserId(orderEntity.getUserId());userBoundVO.setIntegration(orderEntity.getIntegration());userBoundVO.setGrowth(orderEntity.getGrowth());this.rabbitTemplate.convertAndSend("ORDER-EXCHANGE", "bound.plus", userBoundVO);channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);} }給gmall-oms工程的OrderMapper接口及實現類添加successOrder方法:
int successOrder(String orderToken); <update id="successOrder">update oms_order set `status`=1 where order_sn=#{orderToken} and `status`=0 </update>給gmall-oms-interface工程添加UserBoundVO
內容:
@Data public class UserBoundVO {private Long userId;private Integer integration;private Integer growth; }1.4.5.3. wms減庫存
給gmall-wms的StockListener添加減庫存的監聽器方法:
@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "STOCK-MINUS-QUEUE", durable = "true"),exchange = @Exchange(value = "ORDER-EXCHANGE", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC),key = {"stock.minus"} )) public void minusStock(String orderToken, Channel channel, Message message) throws IOException {try {// 獲取redis中該訂單的鎖定庫存信息String json = this.redisTemplate.opsForValue().get(KEY_PREFIX + orderToken);if (StringUtils.isNotBlank(json)){// 反序列化獲取庫存的鎖定信息List<SkuLockVo> skuLockVos = JSON.parseArray(json, SkuLockVo.class);// 遍歷并解鎖庫存信息skuLockVos.forEach(skuLockVo -> {this.wareSkuMapper.minus(skuLockVo.getWareSkuId(), skuLockVo.getCount());});// 刪除redis中庫存鎖定信息this.redisTemplate.delete(KEY_PREFIX + orderToken);}channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);} catch (Exception e) {e.printStackTrace();if (message.getMessageProperties().getRedelivered()){channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);} else {channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);}} }給gmall-wms的WareSkuMapper添加方法:
void minus(@Param("id") Long wareSkuId, @Param("count") Integer count);給gmall-wms的WareSkuMapper.xml添加映射
<update id="minus">update wms_ware_sku set stock_locked = stock_locked - #{count}, stock = stock - #{count}, sales = sales + #{count} where id = #{id} </update>gmall-ums中加積分的監聽器略。。。。。。
1.4.6. 同步回調
用戶掃描支付成功后,我們可以通過同步回調,跳轉到商戶的支付成功頁。
請求方式:GET
請求路徑:/pay/ok
請求參數:參照異步請求(比異步請求略少)
返回視圖名稱
@GetMapping("pay/ok") public String payOk(PayAsyncVo payAsyncVo){// 查詢訂單數據展示在支付成功頁面// String orderToken = payAsyncVo.getOut_trade_no();// TODO:查詢并通過model響應給頁面return "paysuccess"; }2. 秒殺
秒殺具有瞬間高并發的特點,針對這一特點,必須要做限流 + 異步 + 緩存 (+ 頁面靜態化)。
限流方式:
d();
SkuLockVO lockVO = new SkuLockVO();lockVO.setOrderToken(orderSn);lockVO.setCount(1);lockVO.setSkuId(skuId);//準備閉鎖信息RCountDownLatch latch = this.redissonClient.getCountDownLatch("sec:countdown:" + orderSn);latch.trySetCount(1);this.rabbitTemplate.convertAndSend("ORDER-EXCHANGE", "sec.kill", lockVO);return ResponseVo.ok("秒殺成功,訂單號:" + orderSn);}else {return ResponseVo.fail("秒殺失敗,歡迎再次秒殺!");} } return ResponseVo.fail("請登錄后再試!");}
@GetMapping("/miaosha/pay")
public String payKillOrder(String orderSn) throws InterruptedException {
}
總結
以上是生活随笔為你收集整理的尚硅谷2020微服务分布式电商项目《谷粒商城》-支付、秒杀的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于yolov5n的轻量级MSTAR遥感
- 下一篇: 欢聚时代一面