標籤: 網頁設計公司

  • RocketMQ系列(四)順序消費

    RocketMQ系列(四)順序消費

    折騰了好長時間才寫這篇文章,順序消費,看上去挺好理解的,就是消費的時候按照隊列中的順序一個一個消費;而併發消費,則是消費者同時從隊列中取消息,同時消費,沒有先後順序。RocketMQ也有這兩種方式的實現,但是在實踐的過程中,就是不能順序消費,好不容易能夠實現順序消費了,發現採用併發消費的方式,消費的結果也是順序的,頓時就蒙圈了,到底怎麼回事?哪裡出了問題?百思不得其解。

    經過多次調試,查看資料,debug跟蹤程序,最後終於搞清楚了,但是又不知道怎麼去寫這篇文章,是按部就班的講原理,講如何配置到最後實現,還是按照我的調試過程去寫呢?我覺得還是按照我的調試過程去寫這篇文章吧,因為我的調成過程應該和大多數人的理解思路是一致的,大家也更容易重視。

    環境回顧

    我們先來回顧一下前面搭建的RocketMQ的環境,這對於我們理解RocketMQ的順序消費是至關重要的。我們的RocketMQ環境是一個兩主兩從的異步集群,其中有兩個broker,broker-a和broker-b,另外,我們創建了兩個Topic,“cluster-topic”,這個Topic我們在創建的時候指定的是集群,也就是說我們發送消息的時候,如果Topic指定為“cluster-topic”,那麼這個消息應該在broker-a和broker-b之間負載;另外創建的一個Topic是“broker-a-topic”,這個Topic我們在創建的時候指定的是broker-a,當我們發送這個Topic的消息時,這個消息只會在broker-a當中,不會出現在broker-b中。

    和大家羅嗦了這麼多,大家只要記住,我們的環境中有兩個broker,“broker-a”和“broker-b”,有兩個Topic,“cluster-topic”和“broker-a-topic”就可以了。

    cluster-topic可以順序消費嗎

    我們發送的消息,如果指定Topic為“cluster-topic”,那麼這種消息將在broker-a和broker-b直接負載,這種情況能夠做到順序消費嗎?我們試驗一下,

    消費端的代碼如下:

    @Bean(name = "pushConsumerOrderly", initMethod = "start",destroyMethod = "shutdown")
    public DefaultMQPushConsumer pushConsumerOrderly() throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("pushConsumerOrderly");
        consumer.setNamesrvAddr("192.168.73.130:9876;192.168.73.131:9876;192.168.73.132:9876;");
        consumer.subscribe("cluster-topic","*");
        consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
            Random random = new Random();
            try {
                Thread.sleep(random.nextInt(5) * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (MessageExt msg : msgs) {
                System.out.println(new String(msg.getBody()));
            }
            return ConsumeOrderlyStatus.SUCCESS;
        });
        return consumer;
    }
    
    • 消費者組的名稱,連接的NameServer,訂閱的Topic,這裏就不多說了;
    • 再來看一下註冊的消息監聽器,它是MessageListenerOrderly,順序消費,具體實現里我們打印出了消息體的內容,最後返回消費成功ConsumeOrderlyStatus.SUCCESS。
    • 重點看一下打印語句之前的隨機休眠,這是非常重要的一步,它可以驗證消息是否是順序消費的,如果消費者是消費完一個消息以後,再去取下一個消息,那麼順序是沒有問題,但是如果消費者是併發地取消息,但是每個消費者的休眠時間又不一樣,那麼打印出來的就是亂序

    生產端我們採用同步發送的方式,代碼如下:

    @Test
    public void producerTest() throws Exception {
    
        for (int i = 0;i<5;i++) {
            Message message = new Message();
            message.setTopic("cluster-topic");
            message.setKeys("key-"+i);
            message.setBody(("this is simpleMQ,my NO is "+i+"---"+new Date()).getBytes());
    
            SendResult sendResult = defaultMQProducer.send(message);
            System.out.println("i=" + i);
            System.out.println("BrokerName:" + sendResult.getMessageQueue().getBrokerName());
        }
    }
    

    和前面一樣,我們發送5個消息,並且打印出i的值和broker的名稱,發送消息的順序是0,1,2,3,4,發送完成后,我們觀察一下消費端的日誌,如果順序也是0,1,2,3,4,那麼就是順序消費。我們運行一下,看看結果吧。

    生產者的發送日誌如下:

    i=0
    BrokerName:broker-a
    i=1
    BrokerName:broker-a
    i=2
    BrokerName:broker-a
    i=3
    BrokerName:broker-a
    i=4
    BrokerName:broker-b
    

    發送5個消息,其中4個在broker-a,1個在broker-b。再來看看消費端的日誌:

    this is simpleMQ,my NO is 3---Wed Jun 10 13:48:57 CST 2020
    this is simpleMQ,my NO is 2---Wed Jun 10 13:48:57 CST 2020
    this is simpleMQ,my NO is 4---Wed Jun 10 13:48:57 CST 2020
    this is simpleMQ,my NO is 1---Wed Jun 10 13:48:57 CST 2020
    this is simpleMQ,my NO is 0---Wed Jun 10 13:48:56 CST 2020
    

    順序是亂的?怎麼回事?說明消費者在並不是一個消費完再去消費另一個,而是拉取了一個消息以後,並沒有消費完就去拉取下一個消息了,那這不是併發消費嗎?可是我們程序中設置的是順序消費啊。這裏我們就開始懷疑是broker的問題,難道是因為兩個broker引起的?順序消費只能在一個broker里才能實現嗎?那我們使用broker-a-topic這個試一下吧。

    broker-a-topic可以順序消費嗎?

    我們把上面的程序稍作修改,只把訂閱的Topic和發送消息時消息的Topic改為broker-a-topic即可。代碼在這裏就不給大家重複寫了,重啟一下程序,發送消息看看日誌吧。

    生產者端的日誌如下:

    i=0
    BrokerName:broker-a
    i=1
    BrokerName:broker-a
    i=2
    BrokerName:broker-a
    i=3
    BrokerName:broker-a
    i=4
    BrokerName:broker-a
    

    我們看到5個消息都發送到了broker-a中,再來看看消費端的日誌,

    this is simpleMQ,my NO is 0---Wed Jun 10 14:00:28 CST 2020
    this is simpleMQ,my NO is 2---Wed Jun 10 14:00:29 CST 2020
    this is simpleMQ,my NO is 3---Wed Jun 10 14:00:29 CST 2020
    this is simpleMQ,my NO is 4---Wed Jun 10 14:00:29 CST 2020
    this is simpleMQ,my NO is 1---Wed Jun 10 14:00:29 CST 2020
    

    消費的順序還是亂的,這是怎麼回事?消息都在broker-a中了,為什麼消費時順序還是亂的?程序有問題嗎?review了好幾遍沒有發現問題。

    問題排查

    問題卡在這個地方,卡了好長時間,最後在官網的示例中發現,它在發送消息時,使用了一個MessageQueueSelector,我們也實現一下試試吧,改造一下發送端的程序,如下:

    SendResult sendResult = defaultMQProducer.send(message, new MessageQueueSelector() {
        @Override
        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
            return mqs.get(0);
        }
    },i);
    

    在發送的方法中,我們實現了MessageQueueSelector接口中的select方法,這個方法有3個參數,mq的集合,發送的消息msg,和我們傳入的參數,這個參數就是最後的那個變量i,大家不要漏了。這個select方法需要返回的是MessageQueue,也就是mqs變量中的一個,那麼mqs中有多少個MessageQueue呢?我們猜測是2個,因為我們只有broker-a和broker-b,到底是不是呢?我們打斷點看一下,

    MessageQueue有8個,並且brokerName都是broker-a,原來Broker和MessageQueue不是相同的概念,之前我們都理解錯了。我們可以用下面的方式理解,

    集群 ——–》 Broker ————》 MessageQueue

    一個RocketMQ集群里可以有多個Broker,一個Broker里可以有多個MessageQueue,默認是8個。

    那現在對於順序消費,就有了正確的理解了,順序消費是只在一個MessageQueue內,順序消費,我們驗證一下吧,先看看發送端的日誌,

    i=0
    BrokerName:broker-a
    i=1
    BrokerName:broker-a
    i=2
    BrokerName:broker-a
    i=3
    BrokerName:broker-a
    i=4
    BrokerName:broker-a
    

    5個消息都發送到了broker-a中,通過前面的改造程序,這5個消息應該都是在MessageQueue-0當中,再來看看消費端的日誌,

    this is simpleMQ,my NO is 0---Wed Jun 10 14:21:40 CST 2020
    this is simpleMQ,my NO is 1---Wed Jun 10 14:21:41 CST 2020
    this is simpleMQ,my NO is 2---Wed Jun 10 14:21:41 CST 2020
    this is simpleMQ,my NO is 3---Wed Jun 10 14:21:41 CST 2020
    this is simpleMQ,my NO is 4---Wed Jun 10 14:21:41 CST 2020
    

    這回是順序消費了,每一個消費者都是等前面的消息消費完以後,才去消費下一個消息,這就完全解釋的通了,我們再把消費端改成併發消費看看,如下:

    @Bean(name = "pushConsumerOrderly", initMethod = "start",destroyMethod = "shutdown")
    public DefaultMQPushConsumer pushConsumerOrderly() throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("pushConsumerOrderly");
        consumer.setNamesrvAddr("192.168.73.130:9876;192.168.73.131:9876;192.168.73.132:9876;");
        consumer.subscribe("broker-a-topic","*");
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            Random random = new Random();
            try {
                Thread.sleep(random.nextInt(5) * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (MessageExt msg : msgs) {
                System.out.println(new String(msg.getBody()));
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        return consumer;
    }
    

    這回使用的是併發消費,我們再看看結果,

    i=0
    BrokerName:broker-a
    i=1
    BrokerName:broker-a
    i=2
    BrokerName:broker-a
    i=3
    BrokerName:broker-a
    i=4
    BrokerName:broker-a
    

    5個消息都在broker-a中,並且知道它們都在同一個MessageQueue中,再看看消費端,

    this is simpleMQ,my NO is 1---Wed Jun 10 14:28:00 CST 2020
    this is simpleMQ,my NO is 0---Wed Jun 10 14:28:00 CST 2020
    this is simpleMQ,my NO is 3---Wed Jun 10 14:28:00 CST 2020
    this is simpleMQ,my NO is 2---Wed Jun 10 14:28:00 CST 2020
    this is simpleMQ,my NO is 4---Wed Jun 10 14:28:00 CST 2020
    

    是亂序的,說明消費者是併發的消費這些消息的,即使它們在同一個MessageQueue中。

    總結

    好了,到這裏終於把順序消費搞明白了,其中的關鍵就是Broker中還有多個MessageQueue,同一個MessageQueue中的消息才能順序消費。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • Java I/O模型及其底層原理

    Java I/O模型及其底層原理

      Java I/O是Java基礎之一,在面試中也比較常見,在這裏我們嘗試通過這篇文章闡述Java I/O的基礎概念,幫助大家更好的理解Java I/O。
      在剛開始學習Java I/O時,我很迷惑,因為網上絕大多數的文章都是講解Linux網絡I/O模型的,那時我總是搞不明白和Java I/O的關係。後來查了看了好多,才明白Java I/O的原理是以Linux網絡I/O模型為基礎的,理解了Linux網絡I/O模型再學習Java I/O就很方便了,所以這篇文章,我們先來了解I/O的基本概念,再學習Linux網絡I/O模型,最後再看Java中的幾種I/O。

    什麼是I/O?

      I/O是Input、Output的縮寫,即對應計算機中的輸入輸出,以一次文件讀取為例,我們需要將磁盤上的數據讀取到用戶空間,那麼這次數據轉移操作其實就是一次I/O操作,更具體的說是一次文件I/O。我們瀏覽網頁,其中在請求一個網頁時,服務器通過網絡把數據發送給我們,此時程序將數據從TCP緩衝區複製到用戶空間,那麼這次數據轉移操作其實也是一次I/O操作,更具體的說是一次網絡I/O。I/O到處都在,十分重要,Java對I/O對底層操作系統的各種I/O模型進行了封裝,使我們可以輕鬆開發。

    Linux網絡I/O模型

      根據UNIX網絡編程對I/O模型的分類,UNIX提供了5種I/O模型,分別是:阻塞I/O(Blocking I/O)、非阻塞I/O(Non-Blacking I/O)、I/O多路復用模型(I/O Multiplexing)、信號驅動式I/O(Signal Driven I/O)、異步I/O(Asynchronous I/O)。我們逐步了解一下其基本原理。

    阻塞I/O(Blocking I/O)

      阻塞I/O是最早最基礎的I/O模型,其在讀寫數據過程中會阻塞。通過下圖我們可以看到,當用戶進程調用了recvfrom這個系統調用后,內核開始第一階段的數據準備工作,直到內核等待數據準備完成,然後開始第二階段的將數據從內核複製到用戶空間的工作,最後內核返回結果。整個過程中用戶進程都是阻塞的,直到最後返回結果后才接觸阻塞block狀態。阻塞I/O模型適用於併發量小且對時延不敏感的系統。

    非阻塞I/O(Non-Blacking I/O)

      當用戶進程調用recvfrom這個系統調用后,如果內核尚未準備好數據,此時不再阻塞用戶進程,而是立即返回一個EWOULDBLOCK錯誤。用戶進程會不斷髮起系統調用直到內核中數據被準備好(輪詢),此時將執行第二階段的將數據從內核複製到用戶空間的工作,然後內核返回結果。非阻塞I/O模型不斷地輪詢往往需要耗費大量cpu時間。

    I/O多路復用模型(I/O Multiplexing)

      I/O多路復用的優點在於單個進程可以同時處理多個網絡連接的I/O,其基本原理就是select/epoll函數可以不斷的輪詢其負責的所有socket,當某個socket有數據到達時,就通知用戶進程。
      如下圖所示,當用戶進程調用select函數時,整個進程會被阻塞block住,但是這裏的阻塞不是被socket I/O阻塞,而是被select這個函數阻塞。同時內核會監聽改select負責的所有socket(這裏的socket一般設置為non-blocking),當任何一個socket中的數據準備好時,select就會返回給用戶進程,這時候用戶進程再此發起一個系統調用,將數據從內核複製到用戶空間,並返回結果。
      對比I/O多路復用模型和阻塞I/O模型的流程,多路復用多了一個系統調用來完成select環節,除此之外沒有太大的不同。Select的優勢在於它可以同時處理多個connection,但是會多一個系統調用。多路復用本質上也不是非阻塞的。

    信號驅動式I/O(Signal Driven I/O)

      首先我們開啟socket的信號驅動I/O功能,然後用戶進程發起sigaction系統調用給內核后立即返回並可繼續處理其他工作。收到sigaction系統調用的內核在將數據準備好後會按照要求產生一個signo信號通知給用戶進程。然後用戶進程再發起recvfrom系統調用,完成數據從內核到用戶空間的複製,並返回最終結果。其基礎原理圖示如下:

    異步I/O(Asynchronous I/O)

      用戶進程向內核發起系統調用后,就可以開始去做其他事情了。內核收到異步I/O的系統調用后,會直接retrun,所以這裏不會對用戶進程有阻塞。之後內核等待數據準備完成後會繼續將數據從內核拷貝到用戶空間(具體動作可以由異步I/O調用定義),然後內核回給用戶進程發送一個signal,告訴用戶進程I/O操作完成了,整個過程不會導致用戶請求進程阻塞。
      信號驅動I/O模型是內核通知我們可以發起I/O操作了,而異步I/O模式是內核告訴我們I/O操作已經完成了。

      以上就是Linux的5種網絡I/O模型,其中前4中都是同步I/O模型,他們真正的I/O操作環節都會將進程阻塞,只有最後一種異步I/O模型是異步I/O操作。

    Java中的I/O模型

      在JDK1.4之前,基於Java的所有socket通信都是使用阻塞I/O(BIO),JDK1.4提供了了非阻塞I/O(NIO)功能,不過雖然名字叫做NIO,實際底層模型是I/O多路復用,JDK1.7提供了針對異步I/O(AIO)功能。

    BIO

      BIO簡化了上層開發,但是性能瓶頸問題嚴重,對高併發第時延支持差。
    基於消息隊列和線程池技術優化的BIO模式雖然可以對高併發支持有一定幫助,但是還是受限於線程池大小和線程池阻塞隊列大小的制約,當併發數超過線程池的處理能力時,部分請求法務繼續處理,會導致客戶端連接超時,影響用戶體驗。

    NIO

      NIO彌補了BIO的不足,簡單說就是通過selector不斷輪詢註冊在自己上面的channel,如果channel上面有新的連接讀寫時間時就會被輪詢出來,一個selector上面可以註冊多個channel,一個線程就可以負責selector的輪詢,這樣就可以支持成千上萬的連接。Selector就是一個輪詢器,channel是一個通道,通過它來讀取或者寫入數據,通道是雙向的,可以用於讀、寫、讀和寫。Buffer用來和channel交互,數據通過channel進出buffer。
    NIO的優點是可以可靠性好以及高併發低時延,但是使用NIO的代碼開發較為複雜。

    AIO

      AIO,或者說叫做NIO2.0,引入了異步channel的概念,提供了異步文件channel和異步socket channel的實現,開發者可以通過Future類來表示異步操作的結果,也可以在執行異步操作時傳入一個channels,實現CompletionHandler接口作為回調。AIO不用開發者單獨開發獨立線程的selector,異步回調操作有JDK地城思安城池負責驅動,開發起來比NIO簡單一些,同時保持了高可靠高併發低時延的優點。

    參考:
    https://blog.csdn.net/historyasamirror/article/details/5778378
    https://juejin.im/post/5cce5019e51d453a506b0ebf

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

  • .Net Core實戰之基於角色的訪問控制的設計,.Net微服務實戰之技術架構分層篇,.Net微服務實戰之技術選型篇

    .Net Core實戰之基於角色的訪問控制的設計,.Net微服務實戰之技術架構分層篇,.Net微服務實戰之技術選型篇

    前言

      上個月,我寫了兩篇微服務的文章:《.Net微服務實戰之技術架構分層篇》與《.Net微服務實戰之技術選型篇》,微服務系列原有三篇,當我憋第三篇的內容時候一直沒有靈感,因此先打算放一放。

      本篇文章與源碼原本打算實在去年的時候完成併發布的,然而我一直忙於公司項目的微服務的實施,所以該篇文章一拖再拖。如今我花了點時間整理了下代碼,並以此篇文章描述整個實現思路,並開放了源碼給予需要的人一些參考。

      源碼:https://github.com/SkyChenSky/Sikiro.RBAC

    RBAC

      Role-Based Access Contro翻譯成中文就是基於角色的訪問控制,文章以下我都用他的簡稱RBAC來描述。

      現信息系統的權限控制大多數採取RBAC的思想進行實現,其本質思想是對系統各種的操作權限不是直接授予具體的某個用戶,而是在用戶集合與權限集合之間建立一個角色,作為間接關聯。每一種角色對應一組相應的權限。一旦用戶被分配了適當的角色后,該用戶就擁有此角色的所有操作權限。

      通過以上的描述,我們可以分析出以下信息:

    •   用戶與權限是通過角色間接關聯的
    •   角色的本質就是權限組(權限集合)

      這樣做的好處在於,不必在每次創建用戶時都進行分配權限的操作,只要分配用戶相應的角色即可,而且角色的權限變更比用戶的權限變更要少得多,這樣將簡化用戶的權限管理,減少系統的開銷。

      

    功能分析

    權限分類

    從權限的作用可以分為三種,功能權限、訪問權限、數據權限

    • 功能權限
      • 功能權限指系統用戶允許在頁面進行按鈕操作的權限。如果有權限則功能按鈕展示,否則隱藏。
    • 訪問權限
      • 訪問權限指系統用戶通過點擊按鈕後進行地址的請求訪問的權限(地址跳轉與接口請求),如果無權限訪問,則由頁面提示無權限訪問。
    • 數據權限
      • 數據權限指用戶可訪問系統的數據權限,不同的用戶可以訪問不同的數據粒度。

    數據權限的實現可大可小,大可大到對條件進行動態配置,小可小到只針對某個維度進行硬編碼。不納入這次的討論範圍。

    用例圖

    非功能性需求

      時效性,直接影響到安全性,既然是權限控制,那麼理應一修改權限后就立刻生效。曾經有同行問過我,是不是每一個請求都得去查一次數據庫是否滿足權限,如果是,數據庫壓力豈不是很大?

      安全性,每一個頁面跳轉,每一個讀寫請求都的進行一次權限驗證,不滿足的權限的功能按鈕就不需要渲染,避免樣式display:none的情況。

      開發效率,權限控制理應是框架層面的,因此盡可能作為非業務的侵入性,讓開發人員保持原有的數據善增改查與頁面渲染。

    技術選型

    LayUI

      學習門檻極低,開箱即用。其外在極簡,卻又不失飽滿的內在,體積輕盈,組件豐盈,從核心代碼到 API 的每一處細節都經過精心雕琢,非常適合界面的快速開發,它更多是為服務端程序員量身定做,無需涉足各種前端工具的複雜配置,只需面對瀏覽器本身,讓一切你所需要的元素與交互,從這裏信手拈來。作為國人的開源項目,完整的接口文檔與Demo示例讓入門者非常友好的上手,開箱即用的Api讓學習成本盡可能的低,其易用性成為快速開發框架的基礎。

    MongoDB

      主要兩大優勢,無模式與橫向擴展。對於權限模塊來說,無需SQL來寫複雜查詢和報表,也不需要使用到多表的強事務,上面提到的時效性的數據庫壓力問題也可以通過分片解決。無模式使得開發人員無需預定義存儲結構,結合MongoDB官方提供的驅動可以做到快速的開發。

    數據庫設計

     E-R圖

     

      一個管理員可以擁有多個角色,因此管理員與角色是一對多的關聯;角色作為權限組的存在,又可以選擇多個功能權限值與菜單,所以角色與菜單、功能權限值也是一對多的關係。

    類圖

    Deparment與Position屬於非核心,可以按照自己的實際業務進行擴展。

    功能權限值初始化

      隨着業務發展,需求功能是千奇百怪的,根本無法抽象出來,那麼功能按鈕就要隨着業務進行定義。在我的項目里使用了枚舉值進行定義每個功能權限,通過自定義的PermissionAttribute與響應的action進行綁定,在系統啟動時,通過反射把功能權限的枚舉值與相應的controller、action映射到MenuAction表,枚舉值對應code字段,controller與action拼接后對應url字段。

      已初始化到數據庫的權限值可以到菜單頁把相對應的菜單與權限通過用戶界面關聯起來。

    權限值綁定action

    1         [HttpPost]
    2         [Permission(PermCode.Administrator_Edit)]
    3         public IActionResult Edit(EditModel edit)
    4         {
    5             //do something
    6 
    7             return Json(result);
    8         }

    初始化權限值

     1     /// <summary>
     2     /// 功能權限
     3     /// </summary>
     4     public static class PermissionUtil
     5     {
     6         public static readonly Dictionary<string, IEnumerable<int>> PermissionUrls = new Dictionary<string, IEnumerable<int>>();
     7         private static MongoRepository _mongoRepository;
     8 
     9         /// <summary>
    10         /// 判斷權限值是否被重複使用
    11         /// </summary>
    12         public static void ValidPermissions()
    13         {
    14             var codes = Enum.GetValues(typeof(PermCode)).Cast<int>();
    15             var dic = new Dictionary<int, int>();
    16             foreach (var code in codes)
    17             {
    18                 if (!dic.ContainsKey(code))
    19                     dic.Add(code, 1);
    20                 else
    21                     throw new Exception($"權限值 {code} 被重複使用,請檢查 PermCode 的定義");
    22             }
    23         }
    24 
    25         /// <summary>
    26         /// 初始化添加預定義權限值
    27         /// </summary>
    28         /// <param name="app"></param>
    29         public static void InitPermission(IApplicationBuilder app)
    30         {
    31             //驗證權限值是否重複
    32             ValidPermissions();
    33 
    34             //反射被標記的Controller和Action
    35             _mongoRepository = (MongoRepository)app.ApplicationServices.GetService(typeof(MongoRepository));
    36 
    37             var permList = new List<MenuAction>();
    38             var actions = typeof(PermissionUtil).Assembly.GetTypes()
    39                 .Where(t => typeof(Controller).IsAssignableFrom(t) && !t.IsAbstract)
    40                 .SelectMany(t => t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly));
    41 
    42             //遍歷集合整理信息
    43             foreach (var action in actions)
    44             {
    45                 var permissionAttribute =
    46                     action.GetCustomAttributes(typeof(PermissionAttribute), false).ToList();
    47                 if (!permissionAttribute.Any())
    48                     continue;
    49 
    50                 var codes = permissionAttribute.Select(a => ((PermissionAttribute)a).Code).ToArray();
    51                 var controllerName = action?.ReflectedType?.Name.Replace("Controller", "").ToLower();
    52                 var actionName = action.Name.ToLower();
    53 
    54                 foreach (var item in codes)
    55                 {
    56                     if (permList.Exists(c => c.Code == item))
    57                     {
    58                         var menuAction = permList.FirstOrDefault(a => a.Code == item);
    59                         menuAction?.Url.Add($"{controllerName}/{actionName}".ToLower());
    60                     }
    61                     else
    62                     {
    63                         var perm = new MenuAction
    64                         {
    65                             Id = item.ToString().EncodeMd5String().ToObjectId(),
    66                             CreateDateTime = DateTime.Now,
    67                             Url = new List<string> { $"{controllerName}/{actionName}".ToLower() },
    68                             Code = item,
    69                             Name = ((PermCode)item).GetDisplayName() ?? ((PermCode)item).ToString()
    70                         };
    71                         permList.Add(perm);
    72                     }
    73                 }
    74                 PermissionUrls.TryAdd($"{controllerName}/{actionName}".ToLower(), codes);
    75             }
    76 
    77             //業務功能持久化
    78             _mongoRepository.Delete<MenuAction>(a => true);
    79             _mongoRepository.BatchAdd(permList);
    80         }
    81 
    82         /// <summary>
    83         /// 獲取當前路徑
    84         /// </summary>
    85         /// <param name="filterContext"></param>
    86         /// <returns></returns>
    87         public static string CurrentUrl(HttpContext filterContext)
    88         {
    89             var url = filterContext.Request.Path.ToString().ToLower().Trim('/');
    90             return url;
    91         }
    92     }

    關聯菜單與功能權限

    訪問權限

      當所有權限關係關聯上后,用戶訪問系統時,需要對其所有操作進行攔截與實時的權限判斷,我們註冊一個全局的GlobalAuthorizeAttribute,其主要攔截所有已經標識PermissionAttribute的action,查詢該用戶所關聯所有角色的權限是否滿足允許通過。

      我的實現有個細節,給判斷用戶IsSuper==true,也就是超級管理員,如果是超級管理員則繞過所有判斷,可能有人會問為什麼不在角色添加一個名叫超級管理員進行判斷,因為名稱是不可控的,在代碼邏輯里並不知道用戶起的所謂的超級管理員,就是我們需要繞過驗證的超級管理員,假如他叫無敵管理員呢?

     1  /// <summary>
     2     /// 全局的訪問權限控制
     3     /// </summary>
     4     public class GlobalAuthorizeAttribute : System.Attribute, IAuthorizationFilter
     5     {
     6         #region 初始化
     7         private string _currentUrl;
     8         private string _unauthorizedMessage;
     9         private readonly List<string> _noCheckPage = new List<string> { "home/index", "home/indexpage", "/" };
    10 
    11         private readonly AdministratorService _administratorService;
    12         private readonly MenuService _menuService;
    13 
    14         public GlobalAuthorizeAttribute(AdministratorService administratorService, MenuService menuService)
    15         {
    16             _administratorService = administratorService;
    17             _menuService = menuService;
    18         } 
    19         #endregion
    20 
    21         public void OnAuthorization(AuthorizationFilterContext context)
    22         {
    23             context.ThrowIfNull();
    24 
    25             _currentUrl = PermissionUtil.CurrentUrl(context.HttpContext);
    26 
    27             //不需要驗證登錄的直接跳過
    28             if (context.Filters.Count(a => a is AllowAnonymousFilter) > 0)
    29                 return;
    30 
    31             var user = GetCurrentUser(context);
    32             if (user == null)
    33             {
    34                 if (_noCheckPage.Contains(_currentUrl))
    35                     return;
    36 
    37                 _unauthorizedMessage = "登錄失效";
    38 
    39                 if (context.HttpContext.Request.IsAjax())
    40                     NoUserResult(context);
    41                 else
    42                     LogoutResult(context);
    43                 return;
    44             }
    45 
    46             //超級管理員跳過
    47             if (user.IsSuper)
    48                 return;
    49 
    50             //賬號狀態判斷
    51             var administrator = _administratorService.GetById(user.UserId);
    52             if (administrator != null && administrator.Status != EAdministratorStatus.Normal)
    53             {
    54                 if (_noCheckPage.Contains(_currentUrl))
    55                     return;
    56 
    57                 _unauthorizedMessage = "親~您的賬號已被停用,如有需要請您聯繫系統管理員";
    58 
    59                 if (context.HttpContext.Request.IsAjax())
    60                     AjaxResult(context);
    61                 else
    62                     AuthResult(context, 403, GoErrorPage(true));
    63 
    64                 return;
    65             }
    66 
    67             if (_noCheckPage.Contains(_currentUrl))
    68                 return;
    69 
    70             var userUrl = _administratorService.GetUserCanPassUrl(user.UserId);
    71 
    72             // 判斷菜單訪問權限與菜單訪問權限
    73             if (IsMenuPass(userUrl) && IsActionPass(userUrl))
    74                 return;
    75 
    76             if (context.HttpContext.Request.IsAjax())
    77                 AuthResult(context, 200, GetJsonResult());
    78             else
    79                 AuthResult(context, 403, GoErrorPage());
    80         }
    81     }

    功能權限

      在權限驗證通過後,返回view之前,還是利用了Filter進行一個實時的權限查詢,主要把該用戶所擁有功能權限值查詢出來通過ViewData[“PermCodes”]傳到頁面,然後通過razor進行按鈕的渲染判斷。

      然而我在項目中封裝了大部分常用的LayUI控件,主要利用.Net Core的TagHelper進行了封裝,TagHelper內部與ViewData[“PermCodes”]進行判斷是否輸出HTML。

    全局功能權限值查詢

     1 /// <summary>
     2     /// 全局用戶權限值查詢
     3     /// </summary>
     4     public class GobalPermCodeAttribute : IActionFilter
     5     {
     6         private readonly AdministratorService _administratorService;
     7 
     8         public GobalPermCodeAttribute(AdministratorService administratorService)
     9         {
    10             _administratorService = administratorService;
    11         }
    12 
    13         private static AdministratorData GetCurrentUser(HttpContext context)
    14         {
    15             return context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.UserData)?.Value.FromJson<AdministratorData>();
    16         }
    17 
    18 
    19         public void OnActionExecuting(ActionExecutingContext context)
    20         {
    21             ((Controller)context.Controller).ViewData["PermCodes"] = new List<int>();
    22 
    23             if (context.HttpContext.Request.IsAjax())
    24                 return;
    25 
    26             var user = GetCurrentUser(context.HttpContext);
    27             if (user == null)
    28                 return;
    29 
    30             if (user.IsSuper)
    31                 return;
    32 
    33             ((Controller)context.Controller).ViewData["PermCodes"] = _administratorService.GetActionCode(user.UserId).ToList();
    34         }
    35 
    36         public void OnActionExecuted(ActionExecutedContext context)
    37         {
    38         }
    39     }

    LayUI Buttom的TagHelper封裝

     1   [HtmlTargetElement("LayuiButton")]
     2     public class LayuiButtonTag : TagHelper
     3     {
     4         #region 初始化
     5         private const string PermCodeAttributeName = "PermCode";
     6         private const string ClasstAttributeName = "class";
     7         private const string LayEventAttributeName = "lay-event";
     8         private const string LaySubmitAttributeName = "LaySubmit";
     9         private const string LayIdAttributeName = "id";
    10         private const string StyleAttributeName = "style";
    11 
    12         [HtmlAttributeName(StyleAttributeName)]
    13         public string Style { get; set; }
    14 
    15         [HtmlAttributeName(LayIdAttributeName)]
    16         public string Id { get; set; }
    17 
    18         [HtmlAttributeName(LaySubmitAttributeName)]
    19         public string LaySubmit { get; set; }
    20 
    21         [HtmlAttributeName(LayEventAttributeName)]
    22         public string LayEvent { get; set; }
    23 
    24         [HtmlAttributeName(ClasstAttributeName)]
    25         public string Class { get; set; }
    26 
    27         [HtmlAttributeName(PermCodeAttributeName)]
    28         public int PermCode { get; set; }
    29 
    30         [HtmlAttributeNotBound]
    31         [ViewContext]
    32         public ViewContext ViewContext { get; set; }
    33 
    34         #endregion
    35         public override async void Process(TagHelperContext context, TagHelperOutput output)
    36         {
    37             context.ThrowIfNull();
    38             output.ThrowIfNull();
    39 
    40             var administrator = ViewContext.HttpContext.GetCurrentUser();
    41             if (administrator == null)
    42                 return;
    43 
    44             var childContent = await output.GetChildContentAsync();
    45 
    46             if (((List<int>)ViewContext.ViewData["PermCodes"]).Contains(PermCode) || administrator.IsSuper)
    47             {
    48                 foreach (var item in context.AllAttributes)
    49                 {
    50                     output.Attributes.Add(item.Name, item.Value);
    51                 }
    52 
    53                 output.TagName = "a";
    54                 output.TagMode = TagMode.StartTagAndEndTag;
    55                 output.Content.SetHtmlContent(childContent.GetContent());
    56             }
    57             else
    58             {
    59                 output.TagName = "";
    60                 output.TagMode = TagMode.StartTagAndEndTag;
    61                 output.Content.SetHtmlContent("");
    62             }
    63         }
    64     }

     

    視圖代碼

    結尾

      以上就是我本篇分享的內容,項目是以單體應用提供的,方案思路也適用於前後端分離。最後附上幾個系統效果圖

     

     

     

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※別再煩惱如何寫文案,掌握八大原則!

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • .NetCore對接各大財務軟件憑證API——用友系列(2)

    .NetCore對接各大財務軟件憑證API——用友系列(2)

    一. 前言

    今天我們繼續來分析用友系列的第二個產品–U8Cloud2.5 ,apilink方式的API.官網的API文檔地址如下:U8API文檔 因為我們主要是憑證對接,所以使用到的模塊有總賬、基礎檔案這兩個模塊。

    Ps:2.5的財務系統如果不是最新補丁的話,要記得打補丁,不然後續的科目接口會有問題。

    二. API參數

    2.1 遠程訪問財務系統

    如果我們對接的財務系統是公有雲的U8C的話,你會得到一個遠程的財務系統的地址,接着使用UClient工具,即 通過集成友戶通,為企業應用提供了統一的單點登陸支持,支持CA登陸、短信登陸、用戶名/密碼登陸,支持企業用戶系統與友戶通進行綁定,實現統一的用戶登陸服務 的這麼一個工具。

    具體的添加應用的步驟為

    2.2 全局請求頭

    首先,我們必須要在網站內註冊賬號,API集市上有各個接口的詳細說明,我們需要獲取一個apicode參數,每個API模塊在點擊購買后系統會自動分配該模塊的apicode,所以這也就是我們需要兩個不同的apicode.

    基本上U8cloud2.5的版本接口,需要涉及到的請求參數就是這個了,接着我們就可以愉快的進行開發工作了。

    如圖,固定的全局請求頭參數有以下幾個:

    1.authoration–驗證方式;默認是apicode

    2.apicode—模塊的apicode.也就是我們上文中購買模塊后得到的參數.

    3.system—系統類型. 1—測試. 2—正式.

    4.trantype–翻譯類型; 默認為code.即採用編碼模式.

    2.3 基礎檔案

    基礎檔案,我們主要使用到的API接口有科目查詢.

    會計主體賬簿編碼–我們可以從財務系統里獲取,具體的獲取方式如下

    打開U8Client,使用正確的用戶名和密碼登錄財務系統.在企業建模平台–》基礎檔案–》組織機構–》會計主體 一欄,可以看到我們使用的會計主體賬簿編碼.如我們要使用的就是40001-9999

    其中40001為公司編碼,9999為會計方案.可以看到是採用分頁形式來訪問的,所以如果我們要一次性獲取到所有的會計科目,可以採用以下方法。

            public AccountQueryResponse QueryAccount(string pk_subjscheme, string pageIndex, string glorgBookCode)
            {
                var request = new AccountQueryRequest();
                var pms = new Dictionary<string, object>();
                pms.Add("pk_subjscheme", pk_subjscheme);
                pms.Add("glorgbookcode", glorgBookCode);
                pms.Add("page_now", pageIndex);
                pms.Add("page_size", "100");
                request.SetPostParameters(pms);
                return _Client.Excute(request);
            }
    
            public List<U8AccountResult> GetAccountQueryResult(string pk_subjscheme, string pageIndex, string glorgBookCode)
            {
                var list = new List<U8AccountResult>();
                var response = QueryAccount(pk_subjscheme, pageIndex, glorgBookCode);
                if (response != null && response.status == "success" && response.data != null)
                {
                    var result = JsonConvert.DeserializeObject<AccountQueryResult>(response.data);
                    list = result.datas == null ? new List<U8AccountResult>() : result.datas.ToList().Select(x => new U8AccountResult
                    {
                        balanorient = x.accsubjParentVO.balanorient,
                        subjcode = x.accsubjParentVO.subjcode,
                        subjname = x.accsubjParentVO.subjname,
                        dispname = x.accsubjParentVO.dispname,
                        remcode = x.accsubjParentVO.remcode,
                        subjId = x.accsubjParentVO.pk_accsubj,
                        endflag = x.accsubjParentVO.endflag,
                        subjectAssInfos = x.subjass == null ? new List<AccSubjectAssInfo>() : x.subjass.ToList().Select(t => new AccSubjectAssInfo
                        {
                            bdcode = t.bdcode,
                            bddispname = t.bddispname,
                            bdname = t.bdname
                        }).ToList()
                    }).ToList();
                }
                return list;
            }
    
    ///獲取所有的會計科目
            public List<U8AccountResult> GetAllAccount(string pk_subjescheme, string glorgBookCode)
            {
                var pageNo = "1";
                var list = new List<U8AccountResult>();
                var response = QueryAccount(pk_subjescheme, pageNo, glorgBookCode);
                if (response != null && response.status == "success" && response.data != null)
                {
                    var result = JsonConvert.DeserializeObject<AccountQueryResult>(response.data);
                    var allCount = Math.Ceiling(Convert.ToDouble(result.allcount) / result.retcount);
                    if (allCount >= 1)
                    {
                        for (int i = 1; i <= allCount; i++)
                        {
                            var resultList = GetAccountQueryResult(pk_subjescheme, i.ToString(), glorgBookCode);
                            list.AddRange(resultList);
                        }
                    }
                }
                return list;
            }
    

    allCount為總條數,retCount為當次請求的分頁條數,默認最大值為100,即接口每次只能返回100條數據,超過100條的數據量,我們就要採用分頁的形式來獲取了。

    這裏,有兩個隱藏的坑需要注意一下

    1.如果沒有打過類似“patch_會計科目查詢api查詢條件增加會計主體賬簿編碼”這樣的補丁,我們無法傳入會計主體賬簿編碼,就默認返回該集團下所有公司的會計科目,這樣顯然達不到我們的目的。

    2.返回的會計科目中沒有輔助核算明細,這對於我們傳輸憑證也是有影響的。所以這兩個補丁,如果我們在對接的過程中發現有接口有問題,那麼就要聯繫總部的老師幫忙打相應的補丁了.

    2.4 總賬

    總賬模塊,主要是我們的憑證傳輸了.

    我們先來看憑證的保存,憑證保存要傳入相應的憑證json串.

            public GL_VoucherInsertResponse InsertVoucher(List<object> models)
            {
                var request = new GL_VoucherInsertRequest();
                var pms = new Dictionary<string, object>();
                pms.Add("voucher", models);
                request.SetPostParameters(pms);
                return _Client.Excute(request);
            }
    ///憑證新增結果
            public List<U8GLVoucherResult> GetVoucherInsertResult(List<object> models)
            {
                var list = new List<U8GLVoucherResult>();
                var response = InsertVoucher(models);
                if (response != null && response.status == "success")
                {
                    if (response.data != null && !response.data.IsNullOrEmpty())
                    {
                        var result = JsonConvert.DeserializeObject<List<VoucherResult>>(response.data);
                        list = result.Select(x => new U8GLVoucherResult
                        {
                            explanation = x.explanation,
                            glorgbook_code = x.glorgbook_code,
                            glorgbook_name = x.glorgbook_name,
                            no = x.no,
                            pk_glorgbook = x.pk_glorgbook,
                            pk_voucher = x.pk_voucher,
                            totalcredit = x.totalcredit,
                            totaldebit = x.totaldebit,
                            pk_vouchertype = x.pk_vouchertype,
                            vouchertype_code = x.vouchertype_code,
                            vouchertype_name = x.vouchertype_name,
                            prepareddate = Convert.ToDateTime(x.prepareddate),
                            errorMsg = ""
                        }).ToList();
                    }
                }
                else
                {
                    list.Add(new U8GLVoucherResult { errorMsg = response.errormsg });
                }
                return list;
            }
    

    借貸方,憑證字主要用於我們新增后回執進行憑證記錄的.

    接着我們來看憑證保存的實體類.

     public class U8VoucherModel
        {
            /// <summary>
            /// 是否差異憑證
            /// </summary>
            public bool ISDIFFLAG { get; set; }
            /// <summary>
            /// 附單據數
            /// </summary>
            public string attachment { get; set; }
            public Detail[] details { get; set; }
            /// <summary>
            /// 憑證摘要
            /// </summary>
            public string explanation { get; set; }
            /// <summary>
            /// 憑證號
            /// </summary>
            public string no { get; set; }
            /// <summary>
            /// 公司
            /// </summary>
            public string pk_corp { get; set; }
            /// <summary>
            /// 賬簿
            /// </summary>
            public string pk_glorgbook { get; set; }
            /// <summary>
            /// 制單人編碼
            /// </summary>
            public string pk_prepared { get; set; }
            /// <summary>
            /// 憑證類別簡稱
            /// </summary>
            public string pk_vouchertype { get; set; }
            /// <summary>
            /// 制單日期
            /// </summary>
            public string prepareddate { get; set; }
            /// <summary>
            /// 憑證類型
            /// </summary>
            public int voucherkind { get; set; }
        }
    
        public class Detail
        {
            /// <summary>
            /// 原幣貸方金額
            /// </summary>
            public string creditamount { get; set; }
            /// <summary>
            /// 貸方數量
            /// </summary>
            public string creditquantity { get; set; }
            /// <summary>
            /// 原幣借方金額
            /// </summary>
            public string debitamount { get; set; }
            /// <summary>
            /// 借方數量
            /// </summary>
            public string debitquantity { get; set; }
            /// <summary>
            /// 分錄號
            /// </summary>
            public string detailindex { get; set; }
            /// <summary>
            /// 匯率
            /// </summary>
            public string excrate1 { get; set; }
            /// <summary>
            /// 摘要
            /// </summary>
            public string explanation { get; set; }
            /// <summary>
            /// 本幣貸方金額
            /// </summary>
            public string localcreditamount { get; set; }
            /// <summary>
            /// 本幣借方金額
            /// </summary>
            public string localdebitamount { get; set; }
            /// <summary>
            /// 科目
            /// </summary>
            public string pk_accsubj { get; set; }
            /// <summary>
            /// 幣別編碼
            /// </summary>
            public string pk_currtype { get; set; }
            /// <summary>
            /// 單價
            /// </summary>
            public string price { get; set; }
            public Ass[] ass { get; set; }
            public Cashflow[] cashflow { get; set; }
        }
    
        public class Ass
        {
            /// <summary>
            /// 輔助核算類型編碼
            /// </summary>
            public string checktypecode { get; set; }
            /// <summary>
            /// 輔助核算值編碼
            /// </summary>
            public string checkvaluecode { get; set; }
        }
    
        public class Cashflow
        {
            public string cashflow_code { get; set; }
            public string currtype_code { get; set; }
            public int money { get; set; }
        }
    

    整個憑證對接下來,其實坑不是很多,主要在於前期接口文檔的研究,參數的獲取以及測試接口連通性上面.

    三.結束語

    希望文章能在你開發API接口對接的路上一些幫助和解疑,也希望同樣做API對接的小夥伴,我們可以多多交流。祝你在開發的道路上勇往直前。

    我是程序猿貝塔,一個分享自己對接過財務系統API經歷和生活感悟的程序員。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 助長毀林?巴西銷往歐洲的黃豆 至少20%來自森林砍伐

    環境資訊中心綜合外電;姜唯 編譯;林大利 審校

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

  • 心痛!抹香鯨遭漁網纏住痛苦掙扎 潛水員花3天仍割不完

    摘錄自2020年7月22日自由時報報導

    義大利當地時間18日,一條抹香鯨在利帕里島(Lipari)附近海域被廢棄漁網纏住,義大利海警花了數天時間企圖割開漁網,但由於抹香鯨躁動,導致進度緩慢。

    綜合外媒報導,一隻抹香鯨被發現受困於義大利利帕里島海域,牠被廢棄漁網困住無法游離,義大利海警獲報後,即刻出動潛水員救援,他們企圖割開漁網,但由於抹香鯨情緒不穩,相當躁動,令潛水員相當困擾,也因此把牠命為「Fury」(憤怒之意),潛水員花了3天時間才將部分漁網割下。

    不料恢復行動力的「Fury」開始下潛失去蹤影,儘管纏在「Fury」身上的大部分漁網已經割開,不過牠的尾巴仍有魚網纏繞住,救難人員正積極尋找其下落。

    生物多樣性
    海洋
    國際新聞
    抹香鯨
    漁網

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※推薦評價好的iphone維修中心

  • 研究:全球礁鯊數量銳減 部分海域已絕跡

    摘錄自2020年7月22日中央社報導

    22日發布在科學期刊「自然」(Nature)的一份調查,首度全面了解全球哪裡的礁鯊實際上已滅絕。這份調查為期四年,在近60個國家超過370個暗礁海域進行研究。

    加拿大達爾豪希大學(Dalhousie University)副教授麥克尼爾(Aaron MacNeil)表示:「我們原本預期……地球上每個暗礁海域應該都有鯊魚出沒,結果發現我們調查的海域中,有20%沒有任何鯊魚,這令人非常擔心。」

    調查顯示,在卡達、印度、越南及肯亞等八個國家的暗礁海域,完全找不到鯊魚。這不代表這些國家的海域沒有鯊魚,但證明當地暗礁海域的鯊魚數量少得可憐。這表示礁鯊在當地生態系統已失去任何角色,也就是牠們已經功能性滅絕。

    研究指出,破壞性的捕魚活動最可能是礁鯊數量大減罪魁禍首。「使用流刺網和延繩捕魚,對原本數量相對豐沛的礁鯊帶來最大負面影響。」

    生物多樣性
    物種保育
    土地利用
    農林漁牧業
    國際新聞
    全球
    鯊魚
    暗礁
    滅絕
    捕魚
    生態系統
    流刺網
    延繩釣

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

  • 世界經濟論壇:綠色振興將可創造一年300兆元收益

    環境資訊中心綜合外電;姜唯 編譯;林大利 審校

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 印尼帝汶島海灘驚現大鯨魚屍體

    摘錄自2020年7月24日法廣印尼報導

    印度尼西亞南部帝汶島沙灘附近,發現了一具上百噸重大鯨魚的屍體。當局目前還沒搞懂這頭大型哺乳動物的死因。

    當地漁民是在20日早上發現這具長達29米的大鯨魚。海事當局認為這頭海洋生物已有70歲,無生命的漂移,可能已長達數年之久。死因則尚無定論。其屍體上沒有發現任何傷痕,但當地媒體提到了上一個的案例。

    2018年,另一頭鯨魚曾擱淺在附近的另個小島Kapota。那頭鯨魚的胃裡發現了眾多塑膠垃圾:115個塑膠杯,還有4個塑膠瓶。這一發現讓人感到震驚,儘管這最終並沒有被確定是造成大鯨魚死亡的原因,但已足以引起科學界的重視。

    生物多樣性
    海洋
    國際新聞
    印尼
    鯨魚
    海洋垃圾

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

  • 漢娜颶風肆虐德州 吹倒美墨邊境圍牆

    摘錄自2020年7月27日東森國際新聞美國報導

    美國德州(Texas)新冠肺炎疫情持續肆虐,周末又遭颶風漢娜(Hanna)侵襲。漢娜是今年第一個颶風,它已經摧毀許多船隻、淹沒街道、造成電力中斷、甚至吹倒一部分美墨邊境圍牆,災情相當嚴重。

    據外媒《WDSU News》報導,國家颶風中心(National Hurricane Center)表示,目前颶風漢娜已降為熱帶低氣壓,以每小時超過50英里的風速橫越美墨邊境,並在德州南部和墨西哥東北部的部分地區降下了超過300毫米的豪雨。

    州長格雷格·阿博特 (Greg Abbott)於週六(25日)在一場記者會中表示,「任何颶風都是一場巨大的挑戰,但這次的挑戰非常複雜,必且比以往更加嚴峻,因為這場颶風正在席捲著新冠肺炎的其中一個震央。」

    土地利用
    國際新聞
    美國
    颶風
    災害

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※別再煩惱如何寫文案,掌握八大原則!

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!