1. 程式人生 > 實用技巧 >SpringBoot微信點餐專案(四)——其他與總結

SpringBoot微信點餐專案(四)——其他與總結

iwehdio的部落格園:https://www.cnblogs.com/iwehdio/

1、補充

  • 我們希望輸出到前端的JSON資料都是code/msg/data,格式的,但是如果是丟擲異常就會返回Java預設的格式,處理這個問題可以由全域性異常處理:

    @ExceptionHandler(SellException.class)
    @ResponseBody
    public ProductResultVO handlerSell(SellException e) {
        return ProductResultVOUtil.error(e.getCode(), e.getMessage());
    }
    
  • 如果希望返回的HttpResponse是可以設定的狀態碼,可以用註解如@ResponseStatus(HttpStatus.ACCEPTED)

  • Mybatis和JPA的選擇:

    • 建表用SQL,不要用JPA。
    • 表之間的關係在程式中控制,而不是使用@OneToMany等註解進行級聯操作。

2、鎖和快取

  • 壓測模擬Apache ab:

    • 模擬高併發下的情景。

    • 輸入引數:

      abs -n 請求數 -t 時間 -c 併發數 測試的url
      
  • 秒殺相關程式碼:

    • 業務層:

      @Service
      public class SecKillServiceImpl implements SecKillService {
      
          private static final int TIMEOUT = 10 * 1000; //超時時間 10s
      
          @Autowired
          private RedisLock redisLock;
      
          /**
           * 限量100000份
           */
          static Map<String,Integer> products;
          static Map<String,Integer> stock;
          static Map<String,String> orders;
          static
          {
              /**
               * 模擬多個表,商品資訊表,庫存表,秒殺成功訂單表
               */
              products = new HashMap<>();
              stock = new HashMap<>();
              orders = new HashMap<>();
              products.put("123456", 100000);
              stock.put("123456", 100000);
          }
      
          private String queryMap(String productId)
          {
              return "限量份"
                      + products.get(productId)
                      +" 還剩:" + stock.get(productId)+" 份"
                      +" 該商品成功下單使用者數目:"
                      +  orders.size() +" 人" ;
          }
      
          @Override
          public String querySecKillProductInfo(String productId)
          {
              return this.queryMap(productId);
          }
      
          @Override
          public void orderProductMockDiffUser(String productId)
          {
              //加鎖
      
              //1.查詢該商品庫存,為0則活動結束。
              int stockNum = stock.get(productId);
              if(stockNum == 0) {
                  throw new SellException(ResultEnum.ACTIVITY_END);
              }else {
                  //2.下單(模擬不同使用者openid不同)
                  orders.put(KeyUtil.genUniqueKey(),productId);
                  //3.減庫存
                  stockNum =stockNum-1;
                  try {
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  stock.put(productId,stockNum);
              }
      
              //解鎖
      
          }
      }
      
    • 控制器:

      @RestController
      @RequestMapping("/skill")
      public class SecKillController {
      
          @Autowired
          private SecKillService secKillService;
      
          @GetMapping("/query/{productId}")
          public String query(@PathVariable String productId)throws Exception
          {
              return secKillService.querySecKillProductInfo(productId);
          }
      
          @GetMapping("/order/{productId}")
          public String skill(@PathVariable String productId)throws Exception
          {
              secKillService.orderProductMockDiffUser(productId);
              return secKillService.querySecKillProductInfo(productId);
          }
      }
      
  • 壓力測試:

    abs -n 500 -c 20 127.0.0.1/sell/skill/order/123456
    
    • 結果:

    • 可以看出,已經出現了併發環境下的資料問題,需要加鎖。
  • 在售賣方法上使用synchronized:

    • 可以解決不一致的問題,但是太重量級。
    • 無法做到細粒度控制,而且只適合單機的情況。
  • Redis分散式鎖:

    • 可以作為分散式鎖的原因之一是其單執行緒的特性。

    • setnx命令,即set if not exist。

      • 在所要設定的key不存在時,等同於set命令;如果key存在則什麼也不做。
      • 返回的是是否set,可以用!SETNX來加鎖。
    • getset命令。

      • 格式:getset key值 新value值
      • 即按照key值返回value值,然後將該key對應的值設定為新value值。
    • 加鎖:

      • 在redis中儲存的key是商品id,value是當前時間+超時時間,即該鎖的過期時間。
      • 加鎖方法返回的是是否加鎖成功,加鎖成功則進行業務操作,否則不能進行業務操作。
      @Component
      public class RedisLock {
          @Autowired
          private StringRedisTemplate redisTemplate;
      
          public boolean lock(String key,String value){
              //檢視這個商品id是否存在,也就是說這個商品是否被加鎖了,如果沒有被加鎖則加鎖並返回true
              if (redisTemplate.opsForValue().setIfAbsent(key, value)){
                  return true;
              }
              //獲取此時該商品鎖的過期時間,如果小於當前系統時間則說明已經過期,其他執行緒可以重新加鎖
              String currentValue = redisTemplate.opsForValue().get(key);
              if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
                  //這裡的檢查是為了避免多個執行緒同時修改redis中的value後都認為自己獲得了鎖,實際上value會被多次修改,但是隻有一個執行緒返回獲得了鎖
                  String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
                  if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
                      return true;
                  }
              }
              return false;
          }
      }
      
    • 解鎖:

      public void unLock(String key,String value) {
          String currentValue = redisTemplate.opsForValue().get(key);
          if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
              redisTemplate.opsForValue().getOperations().delete(key);
          }
      }
      
    • 使用:

      @Override
      public void orderProductMockDiffUser(String productId)
      {
          //加鎖
          long time = System.currentTimeMillis() + TIMEOUT;
          if (!redisLock.lock(productId,String.valueOf(time))){
              throw new SellException(ResultEnum.CANNT_GET_LOCK);
          }
          //1.查詢該商品庫存,為0則活動結束。
          int stockNum = stock.get(productId);
          if(stockNum == 0) {
              throw new SellException(ResultEnum.ACTIVITY_END);
          }else {
              //2.下單(模擬不同使用者openid不同)
              orders.put(KeyUtil.genUniqueKey(),productId);
              //3.減庫存
              stockNum =stockNum-1;
              try {
                  Thread.sleep(100);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              stock.put(productId,stockNum);
          }
      
          //解鎖
          redisLock.unLock(productId,String.valueOf(time));
      }
      
  • 快取:

    • 快取的命中、失效和更新。

    • 當專案整合了redis時,SpringBoot不會使用預設的Spring Cache而是使用redis。

    • 在啟動類上加註解@EnableCaching

    • 被快取的實體類需要實現序列化介面,並且定義序列化id,可以用GenerateSerialVersionUID外掛生成。

    • 對於返回需要快取的資料的方法上,加@Cacheable註解:

      • cacheNames指定不同方法生成的快取,或者說是不同型別的快取的鍵。
      • key指定相同型別的快取,但是由於傳入的引數不同導致快取不同一般根據方法入參動態指定。不指定時預設為方法入參。
      • 還可以用condition根據方法入參指定使用快取的情況,為true時才快取。
      • unless根據返回結果指定不使用快取的情況,為true時不快取。
      @GetMapping("/list")
      @Cacheable(cacheNames = "product", key = "123")
      public ProductResultVO<ProductCategoryVO> list() {
      }
      
    • @CachePut更新快取,@CacheEvict刪除快取。

3、部署

  • 使用maven的package功能將專案打成jar包,報錯解決

    mvn clean package -Dmaven.test.skip=true
    
  • 參考:把jar專案部署到阿里雲

  • 執行命令:

    nohup java -jar sell-1.0.jar > /dev/null 2>&1 &
    
  • 部署前端:

    • 安裝Nginx:參考

      wget http://nginx.org/download/nginx-1.12.2.tar.gz
      tar -zxvf nginx-1.12.2.tar.gz -C /usr/local/
      # 進入目錄
      ./configure --prefix=/usr/local/nginx
      make && make install
      
  • 然後使用WinSCP把編譯好的前端專案上傳,部署方法與部署在本機上類似。注意編譯前需要將前端配置檔案中的配置改為伺服器的地址。

4、總結

傳統資料操作

  • 資料庫實體類:

    • ProductInfo:商品的具體資訊。
    • ProductCategory:商品的類目資訊。
    • OrderDetail:某個產品訂單的具體資訊。
    • OrderMaster:總訂單,包括多個產品訂單,存有支付者的相關資訊。
  • 資料傳輸實體類(Data Transfer,用於業務體系中的資料傳輸):

    • OrderDTO:總訂單的所有資訊,OrderMaster與資料庫互動,只有產品訂單的索引。DTO物件中組合了產品訂單的列表。
    • CartDTO:商品的id和數量資訊,一個該型別的列表可以看作是購物車。
  • 檢視實體類(View,用於返回給前端JSON):

    • ProductResultVO:返回給前端的JSON格式對應的實體類。
    • ProductCategoryVO:返回給前端的某一類目下的所有商品,其中包含的是ProdutInfoVO物件。
  • 表單實體類(Form,接收前端提交的表單,可能會用到校驗):

    • OrderForm、ProductForm、CategoryForm。
  • Dao層:

    • 因為使用的是JPA,所以預設實現了findOnefindAllsave方法等。
    • ProductInfoDao操作商品資訊,添加了findByProductStatus方法,查詢上架或下架狀態的所有商品。
    • ProductCategoryDao操作商品類目資訊。
    • OrderDetailDao操作產品訂單資訊,添加了findByOrderId方法,根據訂單號獲取具體訂單的列表。
    • OrderMasterDao操作總訂單,添加了findByBuyerOpenid方法,根據買家的openid獲取該使用者所有的主訂單的列表。
    • SellerInfoDao操作能登入賣家端的資訊表,原專案中是用微信登入的openid查詢。
  • Service層:

    • 最基本的要實現增刪改查操作,包括根據主鍵查詢findOne、查詢所有findAll、儲存save(即增和改)。

    • ProductInfoService實現商品資訊相關功能,除了增刪改查外,還有對該產品加減庫存的操作(需要事務支援、商品上架和下架操作。

    • ProductCategoryService實現商品類目資訊相關功能。

    • OrderMasterService實現訂單的相關功能。

      • 根據訂單號查詢,根據買家openid查詢,查詢所有訂單。
      • 建立訂單:
        1. 根據傳入的訂單DTO物件,生成訂單號,初始化總價0,初始化空購物車列表。
        2. 遍歷訂單DTO物件中的商品訂單列表,計算總價,填充購物車,將每個商品訂單物件寫入資料庫。
        3. 將總訂單資訊寫入資料庫。扣除相關商品的庫存。向賣家端推送訊息。
      • 取消訂單:
        1. 根據傳入的訂單DTO物件,查詢訂單是否是可以取消的狀態。
        2. 修改訂單狀態,返還庫存。如果支付了需要退款。
      • 完結訂單:判斷訂單狀態是否可以完結,修改訂單狀態。
      • 建立、取消訂單尤其需要保證資料一致性,用事務控制。
    • BuyerService實現買家的功能,查詢訂單、取消訂單(好像和OrderMasterService有點重複?)。

    • SellerInforService實現賣家登入許可權查詢,根據登入使用者名稱查詢。

  • Controller層:

    • 對於微信vue前端使用JSON傳輸資料,對應freemaker後端使用ModelAndView中攜帶map傳輸資料。
    • BuyerProductController實現買家商品操作。
      • 檢視所有商品:查詢所有上架的商品,將其拼裝為ProductResultVO<ProductCategoryVO>
    • BuyerOrderController實現買家訂單操作:
      • 根據前端傳入的OrderForm建立訂單、根據傳入的openid和orderid檢視訂單詳情或取消訂單。
      • 根據openid和分頁引數檢視訂單列表(這個方法貌似沒用到)。
    • SellerProductController實現賣家商品操作:
      • 根據分頁引數檢視商品列表。
      • 商品上架和下架、修改和新增。
      • 訂單和類名操作類似。
    • SellerOrderController實現賣家訂單操作。
    • SellerCategoryController實現賣家類目操作。
    • SellerLogController實現賣家端登入操作。
      • 登入密碼校驗成功,生成UUID,作為值存入Cookie,作為鍵存入Redis實現的分散式Session。
      • 登出,移除Cookie和Session。
    • SellerAuthAspect:攔截訪問Controller的部分方法,AOP實現賣家端許可權控制。

微信和支付

  • 微信獲取openid:

    • 官方文件:微信接入
    • WeiXinController實現微信授權接入的相關功能。
    • WechatController實現獲取微信openid。
      • 首先設定授權回撥域名,即獲取使用者openid的url需要在此域名下。
      • authorize方法,請求使用者同意獲取code,code是獲取access_token的票據,而access_token中包含了openid。
      • userInfo方法,獲取code後,使用者會重定向到一個自行設定的redirect_url,在這個url下請求access_token,獲取openid。
      • 最後攜帶openid跳轉到其他頁面。url是http://前端地址/#/openid={openid}。
  • PayController和PayService實現了支付寶支付:

    • 官方文件:手機網頁支付接入
    • 建立訂單:
      • 根據支付寶閘道器、AppId和私鑰等建立DefaultAlipayClient。
      • 建立手機網站支付請求AlipayTradeWapPayRequest,設定同步回撥和非同步回撥地址、商品資訊等。
      • 因為支付寶老SDK貌似沒有提供單獨提取url的方法,需要自己去form中擷取。
    • 非同步回撥:
      • 將非同步回撥中收到的引數都存到一個map中。
      • 呼叫SDK驗證簽名。
      • 查詢確認本地資料庫中已經完成支付狀態修改。
      • 返回給支付寶一個成功資訊,告訴其停止傳送非同步回撥訊息。
    • 退款:
      • 與建立訂單類似,不過建立的請求是退款請求AlipayTradeRefundRequest。
      • 本地商戶的訂單號或支付寶交易號必須填一個,此外還需要填本次退款金額。
  • 微信拉起支付寶:

    • 參考:微信公眾號接入支付寶支付開發。而且前面支付寶開發文件中也有微信公眾平臺無法使用支付寶收付款的解決方案。
    • 使用支付寶給出的demo並進行簡單的修改。修改的部分一是需要在網頁中使用支付寶支付後返回微信;二是微信需要判斷支付寶支付完成。
    • 在網頁中使用支付寶支付後返回微信,是使用支付寶的同步回撥,在後端使用一個模板檔案開啟微信。
    • 微信需要判斷支付寶支付完成:
      • 支付時map中還存入一個checkUrl,用於檢查這個orderId的訂單有沒有被支付。
      • 這個checkUrl從確認支付頁面被攜帶入了跳轉頁面。
      • 點選支付完成,如果確實支付完成就會進入訂單詳情頁。url是http://前端地址/#/order/{orderId}。

其他部分

  • 全域性異常:

    • 出現不同的問題丟擲不同型別的異常。
    • 全域性異常控制器根據異常的型別,選擇返回不同的報錯JSON資料或跳轉頁面。
  • Redis:

    • 分散式Session:
      • 使用者訪問時檢測其是否帶有一個鍵為token的Cookie。如果沒有,則需要登入。
      • 登入後,生成一個UUID。返回客戶端一個鍵為token,值為UUID的Cookie;返回Redis一個鍵為UUID,值為使用者名稱的Session。
      • 各個分散式伺服器在查詢Session時都是從Redis中查詢的。
    • 分散式鎖:
      • 鎖就是,將商品id作為鍵,過期時間作為值。
      • 當一個執行緒想要要加鎖時,先檢查是否有鎖。如果沒有鎖則加鎖;如果有鎖,則再檢查鎖是否過期。
      • 如果鎖沒有過期則獲取不到鎖;如果過期了,則獲取到鎖,更新過期時間。同時需要考慮多個執行緒進入修改更新時間的情況。
      • 解鎖時,將同樣的過期時間傳入,相同的解鎖。
      • 這裡老師講的貌似有關小bug?即多個執行緒進入修改更新時間,還用原來的過期時間解鎖是解不開的。
  • 訊息推送:微信模板和WebSocket。

  • 專案檔案結構:

    ├─java
    │  └─cn
    │      └─iwehdio
    │          └─sell
    │              │  SellApplication.java
    │              │  
    │              ├─aspect
    │              │      SellerAuthAspect.java
    │              │      
    │              ├─config
    │              │      AlipayConfig.java
    │              │      WebSocketConfig.java
    │              │      WechatAccountConfig.java
    │              │      WechatMpConfig.java
    │              │      
    │              ├─controller
    │              │      BuyerOrderController.java
    │              │      BuyerProductController.java
    │              │      PayController.java
    │              │      SecKillController.java
    │              │      SellerCategoryController.java
    │              │      SellerLogController.java
    │              │      SellerOrderController.java
    │              │      SellerProductController.java
    │              │      WechatController.java
    │              │      WeiXinController.java
    │              │      
    │              ├─converter
    │              │      OrderForm2OrderDTOConverter.java
    │              │      OrderMaster2OrderDTOConverter.java
    │              │      
    │              ├─dao
    │              │      OrderDetailDao.java
    │              │      OrderMasterDao.java
    │              │      ProductCategoryDao.java
    │              │      ProductInfoDao.java
    │              │      SellerInfoDao.java
    │              │      
    │              ├─dataObject
    │              │      OrderDetail.java
    │              │      OrderMaster.java
    │              │      ProductCategory.java
    │              │      ProductInfo.java
    │              │      SellerInfo.java
    │              │      
    │              ├─dto
    │              │      CartDTO.java
    │              │      OrderDTO.java
    │              │      
    │              ├─enums
    │              │      CodeEnum.java
    │              │      OrderStatusEnum.java
    │              │      PayStatusEnum.java
    │              │      ProductStatusEnum.java
    │              │      ResultEnum.java
    │              │      
    │              ├─exception
    │              │      SellerAuthException.java
    │              │      SellException.java
    │              │      
    │              ├─form
    │              │      CategoryForm.java
    │              │      OrderForm.java
    │              │      ProductForm.java
    │              │      
    │              ├─handler
    │              │      SellerAuthExceptionHandler.java
    │              │      
    │              ├─service
    │              │  │  BuyerService.java
    │              │  │  OrderMasterService.java
    │              │  │  PayService.java
    │              │  │  ProductCategoryService.java
    │              │  │  ProductInfoService.java
    │              │  │  PushMessageService.java
    │              │  │  RedisLock.java
    │              │  │  SecKillService.java
    │              │  │  SellerInfoService.java
    │              │  │  WebSocket.java
    │              │  │  
    │              │  └─impl
    │              │          BuyerServiceImpl.java
    │              │          OrderMasterServiceImpl.java
    │              │          PayServiceImpl.java
    │              │          ProductCategoryServiceImpl.java
    │              │          ProductInfoServiceImpl.java
    │              │          PushMessageServiceImpl.java
    │              │          SecKillServiceImpl.java
    │              │          SellerInforServiceImpl.java
    │              │          
    │              ├─utils
    │              │      CookieUtil.java
    │              │      Date2LongSerializer.java
    │              │      EnumUtil.java
    │              │      KeyUtil.java
    │              │      ProductResultVOUtil.java
    │              │      
    │              └─viewObject
    │                      ProductCategoryVO.java
    │                      ProductResultVO.java
    │                      ProdutInfoVO.java
    │                      
    └─resources
        │  application.properties
        │  application.yml
        │  logback-spring.xml
        │  
        ├─static
        │  ├─api
        │  │      ratings.json
        │  │      seller.json
        │  │      
        │  ├─css
        │  │      style.css
        │  │      
        │  ├─js
        │  │      ap.js
        │  │      
        │  └─pay
        │          demo_get.htm
        │          demo_post.htm
        │          pay.htm
        │          
        └─templates
            │  confirm_order.ftl
            │  jumpback.ftl
            │  payfail.ftl
            │  paysuccess.ftl
            │  
            ├─buyer
            ├─common
            │      error.ftl
            │      nav.ftl
            │      success.ftl
            │      
            ├─login
            │      login.ftl
            │      
            ├─sellerCategory
            │      index.ftl
            │      list.ftl
            │      
            ├─sellerOrder
            │      detail.ftl
            │      list.ftl
            │      
            └─sellerProduct
                    index.ftl
                    list.ftl
    
  • 還存在的問題:

    1. 微信前端還有許多問題,以及可以增加的功能。但是前端學的太差了還不會vue,暫時擱置。前端程式碼來自sell_fe_buyer
    2. 許可權控制可以用Shiro或者Spring Security。
    3. 可複用程式碼的抽取,使用設計模式解耦,業務層與控制器層角色分明等。
    4. Entity、DTO、VO、Form實體類的角色明確。

iwehdio的部落格園:https://www.cnblogs.com/iwehdio/