開發與維運

Serverless 在 SaaS 領域的最佳實踐

頭圖.png

作者 | 計緣
來源|阿里巴巴雲原生公眾號

隨著互聯網人口紅利逐漸減弱,基於流量的增長已經放緩,互聯網行業迫切需要找到一片足以承載自身持續增長的新藍海,產業互聯網正是這一宏大背景下的新趨勢。我們看到互聯網浪潮正在席捲傳統行業,雲計算、大數據、人工智能開始大規模融入到金融、製造、物流、零售、文娛、教育、醫療等行業的生產環節中,這種融合稱為產業互聯網。而在產業互聯網中,有一塊不可小覷的領域是 SaaS 領域,它是 ToB 賽道的中間力量,比如 CRM、HRM、費控系統、財務系統、協同辦公等等。

SaaS 系統面臨的挑戰

在消費互聯網時代,大家是搜索想要的東西,各個廠商在雲計算、大數據、人工智能等技術基座之上建立流量最大化的服務與生態,基於海量內容分發與流量共享為邏輯構建系統。而到了產業互聯網時代,供給關係發生了變化,大家是定製想要的東西,需要從供給與需求兩側出發進行雙向建設,這個時候系統的靈活性和擴展性面臨著前所未有的挑戰,尤其是 ToB 的 SaaS 領域。

1.png

特別是對於當下的經濟環境,SaaS 廠商要明白,不能再通過燒錢的方式,只關注在自己的用戶數量上,而更多的要思考如何幫助客戶降低成本、增加效率,所以需要將更多的精力放在自己產品的定製化能力上。

2.png

如何應對挑戰

SaaS 領域中的佼佼者 Salesforce,將 CRM 的概念擴展到 Marketing、Sales、Service,而這三塊領域中只有 Sales 有專門的 SaaS 產品,其他兩個領域都是各個 ISV 在不同行業的行業解決方案,靠的是什麼?毋庸置疑,是 Salesforce 強大的 aPaaS 平臺。ISV、內部實施、客戶均可以在各自維度通過 aPaaS 平臺構建自己行業、自己領域的 SaaS 系統,建立完整的生態。所以在我看來,現在的 Salesforce 已經由一家 SaaS 公司昇華為一家 aPaaS 平臺公司了。這種演進的過程也印證了消費互聯網和產業互聯網的轉換邏輯以及後者的核心訴求。

然而不是所有 SaaS 公司都有財力和時間去孵化和打磨自己的 aPaaS 平臺,但市場的變化、用戶的訴求是實實在在存在的。若要生存,就要求變。這個變的核心就是能夠讓自己目前的 SaaS 系統變得靈活起來,相對建設困難的 aPaaS 平臺,我們其實可以選擇輕量且有效的 Serverless 方案來提升現有系統的靈活性和可擴展性,從而實現用戶不同的定製需求。

Serverless 工作流

在上一篇文章《資源成本雙優化!看 Serverless 顛覆編程教育的創新實踐》中,已經對 Serverless 的概念做過闡述了,並且也介紹了 Serverless 函數計算(FC)的概念和實踐。這篇文章中介紹一下構建系統靈活性的核心要素服務編排—— Serverless 工作流。

Serverless 工作流是一個用來協調多個分佈式任務執行的全託管雲服務。在 Serverless工作流中,可以用順序、分支、並行等方式來編排分佈式任務,Serverless 工作流會按照設定好的步驟可靠地協調任務執行,跟蹤每個任務的狀態轉換,並在必要時執行您定義的重試邏輯,以確保工作流順利完成。Serverless 工作流通過提供日誌記錄和審計來監視工作流的執行,可以輕鬆地診斷和調試應用。

下面這張圖描述了 Serverless 工作流如何協調分佈式任務,這些任務可以是函數、已集成雲服務 API、運行在虛擬機或容器上的程序。

3.png

看完 Serverless 工作流的介紹,大家可能已經多少有點思路了吧。系統靈活性和可擴展性的核心是服務可編排,無論是以前的 BPM 還是現在的 aPaaS。所以基於 Serverless 工作流重構 SaaS 系統靈活性方案的核心思路,是將系統內用戶最希望定製的功能進行梳理、拆分、抽離,再配合函數計算(FC)提供無狀態的能力,通過 Serverless 工作流進行這些功能點的編排,從而實現不同的業務流程。

通過函數計算 FC 和 Serverless 工作流搭建靈活的訂餐模塊

訂餐場景相信大家都不會陌生,在家叫外賣或者在餐館點餐,都涉及到這個場景。當下也有很多提供點餐系統的 SaaS 服務廠商,有很多不錯的 SaaS 點餐系統。隨著消費互聯網向產業互聯網轉換,這些 SaaS 點餐系統面臨的定製化的需求也越來越多,其中有一個需求是不同的商家在支付時會顯示不同的支付方式,比如從 A 商家點餐後付款時顯示支付寶、微信支付、銀聯支付,從 B 商家點餐後付款時顯示支付寶、京東支付。突然美團又冒出來了美團支付,此時 B 商家接了美團支付,那麼從 B 商家點餐後付款時顯示支付寶、京東支付、美團支付。諸如此類的定製化需求越來越多,這些 SaaS 產品如果沒有 PaaS 平臺,那麼就會疲於不斷的通過硬代碼增加條件判斷來實現不同商家的需求,這顯然不是一個可持續發展的模式。

那麼我們來看看通過函數計算 FC 和 Serverless 工作流如何優雅的解決這個問題。先來看看這個點餐流程:

4.png

1. 通過 Serverless 工作流創建流程

首選我需要將上面用戶側的流程轉變為程序側的流程,此時就需要使用 Serverless 工作流來擔任此任務了。

打開 Serverless 控制檯,創建訂餐流程,這裡 Serverless 工作流使用流程定義語言 FDL 創建工作流,如何使用 FDL 創建工作流請參閱文檔。流程圖如下圖所示:

5.png

FDL 代碼為:

version: v1beta1
type: flow
timeoutSeconds: 3600
steps:
  - type: task
    name: generateInfo
    timeoutSeconds: 300
    resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
    pattern: waitForCallback
    inputMappings:
      - target: taskToken
        source: $context.task.token
      - target: products
        source: $input.products
      - target: supplier
        source: $input.supplier
      - target: address
        source: $input.address
      - target: orderNum
        source: $input.orderNum
      - target: type
        source: $context.step.name
    outputMappings:
      - target: paymentcombination
        source: $local.paymentcombination
      - target: orderNum
        source: $local.orderNum
    serviceParams:
      MessageBody: $
      Priority: 1
    catch:
      - errors:
          - FnF.TaskTimeout
        goto: orderCanceled
  -type: task
    name: payment
    timeoutSeconds: 300
    resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
    pattern: waitForCallback
    inputMappings:
      - target: taskToken
        source: $context.task.token
      - target: orderNum
        source: $local.orderNum
      - target: paymentcombination
        source: $local.paymentcombination
      - target: type
        source: $context.step.name
    outputMappings:
      - target: paymentMethod
        source: $local.paymentMethod
      - target: orderNum
        source: $local.orderNum
      - target: price
        source: $local.price
      - target: taskToken
        source: $input.taskToken
    serviceParams:
      MessageBody: $
      Priority: 1
    catch:
      - errors:
          - FnF.TaskTimeout
        goto: orderCanceled
  - type: choice
    name: paymentCombination
    inputMappings:
      - target: orderNum
        source: $local.orderNum
      - target: paymentMethod
        source: $local.paymentMethod
      - target: price
        source: $local.price
      - target: taskToken
        source: $local.taskToken
    choices:
      - condition: $.paymentMethod == "zhifubao"
        steps:
          - type: task
            name: zhifubao
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
            inputMappings:
              - target: price
                source: $input.price            
              - target: orderNum
                source: $input.orderNum
              - target: paymentMethod
                source: $input.paymentMethod
              - target: taskToken
                source: $input.taskToken
      - condition: $.paymentMethod == "weixin"
        steps:
          - type: task
            name: weixin
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
            inputMappings:
            - target: price
              source: $input.price            
            - target: orderNum
              source: $input.orderNum
            - target: paymentMethod
              source: $input.paymentMethod
            - target: taskToken
              source: $input.taskToken
      - condition: $.paymentMethod == "unionpay"
        steps:
          - type: task
            name: unionpay
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
            inputMappings:
            - target: price
              source: $input.price            
            - target: orderNum
              source: $input.orderNum
            - target: paymentMethod
              source: $input.paymentMethod
            - target: taskToken
              source: $input.taskToken
    default:
      goto: orderCanceled
  - type: task
    name: orderCompleted
    resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/orderCompleted
    end: true
  - type: task
    name: orderCanceled
    resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/cancerOrder

在解析整個流程之前,我先要說明的一點是,我們不是完全通過 Serverless 函數計算和 Serverless 工作流來搭建訂餐模塊,只是用它來解決靈活性的問題,所以這個示例的主體應用是 Java 編寫的,然後結合了 Serverless 函數計算和 Serverless 工作流。下面我們來詳細解析這個流程。

2. 啟動流程

按常理,開始點餐時流程就應該啟動了,所以在這個示例中,我的設計是當我們選擇完商品和商家、填完地址後啟動流程:

6.png

這裡我們通過 Serverless 工作流提供的 OpenAPI 來啟動流程。

7.png

  • Java 啟動流程

這個示例我使用 Serverless 工作流的 Java SDK,首先在 POM 文件中添加依賴:

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-core</artifactId>
    <version>[4.3.2,5.0.0)</version>
</dependency>
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-fnf</artifactId>
    <version>[1.0.0,5.0.0)</version>
</dependency>

然後創建初始化 Java SDK 的 Config 類:

@Configuration
public class FNFConfig {

    @Bean
    public IAcsClient createDefaultAcsClient(){
        DefaultProfile profile = DefaultProfile.getProfile(
                "cn-xxx",          // 地域ID
                "ak",      // RAM 賬號的AccessKey ID
                "sk"); // RAM 賬號Access Key Secret
        IAcsClient client = new DefaultAcsClient(profile);
        return client;
    }

}

再來看 Controller 中的 startFNF 方法,該方法暴露 GET 方式的接口,傳入三個參數:

  • fnfname:要啟動的流程名稱。

  • execuname:流程啟動後的流程實例名稱。

  • input:啟動輸入參數,比如業務參數。

   @GetMapping("/startFNF/{fnfname}/{execuname}/{input}")
    public StartExecutionResponse startFNF(@PathVariable("fnfname") String fnfName,
                                           @PathVariable("execuname") String execuName,
                                           @PathVariable("input") String inputStr) throws ClientException {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("fnfname", fnfName);
        jsonObject.put("execuname", execuName);
        jsonObject.put("input", inputStr);
        return fnfService.startFNF(jsonObject);
    }

再來看 Service 中的 startFNF 方法,該方法分兩部分,第一個部分是啟動流程,第二部分是創建訂單對象,並模擬入庫(示例中是放在 Map 裡了):

    @Override
    public StartExecutionResponse startFNF(JSONObject jsonObject) throws ClientException {
        StartExecutionRequest request = new StartExecutionRequest();
        String orderNum = jsonObject.getString("execuname");
        request.setFlowName(jsonObject.getString("fnfname"));
        request.setExecutionName(orderNum);
        request.setInput(jsonObject.getString("input"));

        JSONObject inputObj = jsonObject.getJSONObject("input");
        Order order = new Order();
        order.setOrderNum(orderNum);
        order.setAddress(inputObj.getString("address"));
        order.setProducts(inputObj.getString("products"));
        order.setSupplier(inputObj.getString("supplier"));
        orderMap.put(orderNum, order);

        return iAcsClient.getAcsResponse(request);
    }

啟動流程時,流程名稱和啟動流程實例的名稱是需要傳入的參數,這裡我將每次的訂單編號作為啟動流程的實例名稱。至於 Input,可以根據需求構造 JSON 字符串傳入。這裡我將商品、商家、地址、訂單號構造了 JSON 字符串在流程啟動時傳入流程中。

另外,創建了此次訂單的 Order 實例,並存在 Map 中,模擬入庫,後續環節還會查詢該訂單實例更新訂單屬性。

  • VUE 選擇商品/商家頁面

前端我使用 VUE 搭建,當點擊選擇商品和商家頁面中的下一步後,通過 GET 方式調用 HTTP 協議的接口/startFNF/{fnfname}/{execuname}/{input}。和上面的 Java 方法對應。

  • fnfname:要啟動的流程名稱。

  • execuname:隨機生成 uuid,作為訂單的編號,也作為啟動流程實例的名稱。

  • input:將商品、商家、訂單號、地址構建為 JSON 字符串傳入流程。

            submitOrder(){
                const orderNum = uuid.v1()
                this.$axios.$get('/startFNF/OrderDemo-Jiyuan/'+orderNum+'/{\n' +
                    '  "products": "'+this.products+'",\n' +
                    '  "supplier": "'+this.supplier+'",\n' +
                    '  "orderNum": "'+orderNum+'",\n' +
                    '  "address": "'+this.address+'"\n' +
                    '}' ).then((response) => {
                    console.log(response)
                    if(response.message == "success"){
                        this.$router.push('/orderdemo/' + orderNum)
                    }
                })
            }

3. generateInfo 節點

第一個節點 generateInfo,先來看看 FDL 的含義:

  - type: task
    name: generateInfo
    timeoutSeconds: 300
    resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
    pattern: waitForCallback
    inputMappings:
      - target: taskToken
        source: $context.task.token
      - target: products
        source: $input.products
      - target: supplier
        source: $input.supplier
      - target: address
        source: $input.address
      - target: orderNum
        source: $input.orderNum
      - target: type
        source: $context.step.name
    outputMappings:
      - target: paymentcombination
        source: $local.paymentcombination
      - target: orderNum
        source: $local.orderNum
    serviceParams:
      MessageBody: $
      Priority: 1
    catch:
      - errors:
          - FnF.TaskTimeout
        goto: orderCanceled
```

- name:節點名稱。

- timeoutSeconds:超時時間。該節點等待的時長,超過時間後會跳轉到 goto 分支指向的 orderCanceled 節點。

- pattern:設置為 waitForCallback,表示需要等待確認。inputMappings:該節點入參。

   - taskToken:Serverless 工作流自動生成的 Token。
   - products:選擇的商品。
   - supplier:選擇的商家。
   - address:送餐地址。
   - orderNum:訂單號。
- outputMappings:該節點的出參。

   - paymentcombination:該商家支持的支付方式。
   - orderNum:訂單號。
- catch:捕獲異常,跳轉到其他分支。

這裡 resourceArn 和 serviceParams 需要拿出來單獨解釋。Serverless 工作流支持與多個雲服務集成,即:將其他服務作為任務步驟的執行單元。服務集成方式由 FDL 語言表達,在任務步驟中,可以使用 resourceArn 來定義集成的目標服務,使用 pattern 定義集成模式。所以可以看到在 resourceArn 中配置 acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages 信息,即在 generateInfo 節點中集成了 MNS 消息隊列服務,當 generateInfo 節點觸發後會向 generateInfo-fnf-demo-jiyuanTopic 中發送一條消息。那麼消息正文和參數則在 serviceParams 對象中指定。MessageBody 是消息正文,配置 $ 表示通過輸入映射 inputMappings 產生消息正文。

看完第一個節點的示例,大家可以看到,在 Serverless 工作流中,節點之間的信息傳遞可以通過集成 MNS 發送消息來傳遞,也是使用比較廣泛的方式之一。

## 4. generateInfo-fnf-demo 函數

向 generateInfo-fnf-demo-jiyuanTopic 中發送的這條消息包含了商品信息、商家信息、地址、訂單號,表示一個下訂單流程的開始,既然有發消息,那麼必然有接受消息進行後續處理。所以打開函數計算控制檯,創建服務,在服務下創建名為 generateInfo-fnf-demo 的事件觸發器函數,這裡選擇 Python Runtime:

![8.png](https://ucc.alicdn.com/pic/developer-ecology/291e93740e48446197f29aa179a5f7cb.png)

創建 MNS 觸發器,選擇監聽 generateInfo-fnf-demo-jiyuanTopic。

![9.png](https://ucc.alicdn.com/pic/developer-ecology/880c91561a6747d59c29bb7894eb727a.png)

打開消息服務 MNS 控制檯,創建 generateInfo-fnf-demo-jiyuanTopic:

![10.png](https://ucc.alicdn.com/pic/developer-ecology/e9ab6266f1f046e08a78b9144c797db0.png)

做好函數的準備工作,我們來開始寫代碼:

-- coding: utf-8 --

import logging
import json
import time
import requests
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest

def handler(event, context):

1. 構建Serverless工作流Client

region = "cn-hangzhou"
account_id = "XXXX"
ak_id = "XXX"
ak_secret = "XXX"
fnf_client = AcsClient(
    ak_id,
    ak_secret,
    region
)
logger = logging.getLogger()
# 2. event內的信息即接受到Topic generateInfo-fnf-demo-jiyuan中的消息內容,將其轉換為Json對象
bodyJson = json.loads(event)
logger.info("products:" + bodyJson["products"])
logger.info("supplier:" + bodyJson["supplier"])
logger.info("address:" + bodyJson["address"])
logger.info("taskToken:" + bodyJson["taskToken"])
supplier = bodyJson["supplier"]
taskToken = bodyJson["taskToken"]
orderNum = bodyJson["orderNum"]
# 3. 判斷什麼商家使用什麼樣的支付方式組合,這裡的示例比較簡單粗暴,正常情況下,應該使用元數據配置的方式獲取
paymentcombination = ""
if supplier == "haidilao":
    paymentcombination = "zhifubao,weixin"
else:
    paymentcombination = "zhifubao,weixin,unionpay"

# 4. 調用Java服務暴露的接口,更新訂單信息,主要是更新支付方式
url = "http://xx.xx.xx.xx:8080/setPaymentCombination/" + orderNum + "/" + paymentcombination + "/0"
x = requests.get(url)

# 5. 給予generateInfo節點響應,並返回數據,這裡返回了訂單號和支付方式
output = "{\"orderNum\": \"%s\", \"paymentcombination\":\"%s\" " \
                     "}" % (orderNum, paymentcombination)
request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
request.set_Output(output)
request.set_TaskToken(taskToken)
resp = fnf_client.do_action_with_exception(request)
return 'hello world'

因為 generateInfo-fnf-demo 函數配置了 MNS 觸發器,所以當 TopicgenerateInfo-fnf-demo-jiyuan 有消息後就會觸發執行 generateInfo-fnf-demo 函數。

整個代碼分五部分:

- 構建 Serverless 工作流 Client。

- event 內的信息即接受到 TopicgenerateInfo-fnf-demo-jiyuan 中的消息內容,將其轉換為 Json 對象。

- 判斷什麼商家使用什麼樣的支付方式組合,這裡的示例比較簡單粗暴,正常情況下,應該使用元數據配置的方式獲取。比如在系統內有商家信息的配置功能,通過在界面上配置該商家支持哪些支付方式,形成元數據配置信息,提供查詢接口,在這裡進行查詢。

- 調用 Java 服務暴露的接口,更新訂單信息,主要是更新支付方式。

- 給予 generateInfo 節點響應,並返回數據,這裡返回了訂單號和支付方式。因為該節點的 pattern 是 waitForCallback,所以需要等待響應結果。

## 5. payment 節點

我們再來看第二個節點 payment,先來看 FDL 代碼:
  • type: task
    name: payment
    timeoutSeconds: 300
    resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
    pattern: waitForCallback
    inputMappings:

    • target: taskToken
      source: $context.task.token
    • target: orderNum
      source: $local.orderNum
      - target: paymentcombination
      source: $local.paymentcombination
    • target: type
      source: $context.step.name
      outputMappings:
      - target: paymentMethod
      source: $local.paymentMethod
    • target: orderNum
      source: $local.orderNum
      - target: price
      source: $local.price
    • target: taskToken
      source: $input.taskToken
      serviceParams:
      MessageBody: $
      Priority: 1
      catch:
    • errors:
      • FnF.TaskTimeout
        goto: orderCanceled

當流程流轉到 payment 節點後,意味著用戶進入了支付頁面。

11.png

這時 payment 節點會向 MNS 的 Topicpayment-fnf-demo-jiyuan 發送消息,會觸發 payment-fnf-demo 函數。

6. payment-fnf-demo 函數

payment-fnf-demo 函數的創建方式和 generateInfo-fnf-demo 函數類似,這裡不再累贅。我們直接來看代碼:

# -*- coding: utf-8 -*-
import logging
import json
import os
import time
import logging
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkcore.client import AcsClient
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
from mns.account import Account  # pip install aliyun-mns
from mns.queue import *

def handler(event, context):
    logger = logging.getLogger()
    region = "xxx"
    account_id = "xxx"
    ak_id = "xxx"
    ak_secret = "xxx"
    mns_endpoint = "http://your_account_id.mns.cn-hangzhou.aliyuncs.com/"
    queue_name = "payment-queue-fnf-demo"
    my_account = Account(mns_endpoint, ak_id, ak_secret)
    my_queue = my_account.get_queue(queue_name)
    # my_queue.set_encoding(False)
    fnf_client = AcsClient(
        ak_id,
        ak_secret,
        region
    )
    eventJson = json.loads(event)

    isLoop = True
    while isLoop:
        try:
            recv_msg = my_queue.receive_message(30)
            isLoop = False
            # body = json.loads(recv_msg.message_body)
            logger.info("recv_msg.message_body:======================" + recv_msg.message_body)
            msgJson = json.loads(recv_msg.message_body)
            my_queue.delete_message(recv_msg.receipt_handle)
            # orderCode = int(time.time())
            task_token = eventJson["taskToken"]
            orderNum = eventJson["orderNum"]
            output = "{\"orderNum\": \"%s\", \"paymentMethod\": \"%s\", \"price\": \"%s\" " \
                         "}" % (orderNum, msgJson["paymentMethod"], msgJson["price"])
            request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
            request.set_Output(output)
            request.set_TaskToken(task_token)
            resp = fnf_client.do_action_with_exception(request)
        except Exception as e:
            logger.info("new loop")
    return 'hello world'

該函數的核心思路是等待用戶在支付頁面選擇某個支付方式確認支付。所以這裡使用了 MNS 的隊列來模擬等待。循環等待接收隊列 payment-queue-fnf-demo 中的消息,當收到消息後將訂單號和用戶選擇的具體支付方式以及金額返回給 payment 節點。

7. VUE 選擇支付方式頁面

因為經過 generateInfo 節點後,該訂單的支付方式信息已經有了,所以對於用戶而言,當填完商品、商家、地址後,跳轉到的頁面就是該確認支付頁面,並且包含了該商家支持的支付方式。

12.png

當進入該頁面後,會請求 Java 服務暴露的接口,獲取訂單信息,根據支付方式在頁面上顯示不同的支付方式。代碼片段如下:

13.png

當用戶選定某個支付方式點擊提交訂單按鈕後,向 payment-queue-fnf-demo 隊列發送消息,即通知 payment-fnf-demo 函數繼續後續的邏輯。

這裡我使用了一個 HTTP 觸發器類型的函數,用於實現向 MNS 發消息的邏輯,paymentMethod-fnf-demo 函數代碼如下。

# -*- coding: utf-8 -*-

import logging
import urllib.parse
import json
from mns.account import Account  # pip install aliyun-mns
from mns.queue import *
HELLO_WORLD = b'Hello world!\n'

def handler(environ, start_response):
    logger = logging.getLogger() 
    context = environ['fc.context']
    request_uri = environ['fc.request_uri']
    for k, v in environ.items():
      if k.startswith('HTTP_'):
        # process custom request headers
        pass
    try:       
        request_body_size = int(environ.get('CONTENT_LENGTH', 0))   
    except (ValueError):       
        request_body_size = 0  
    request_body = environ['wsgi.input'].read(request_body_size) 
    paymentMethod = urllib.parse.unquote(request_body.decode("GBK"))
    logger.info(paymentMethod)
    paymentMethodJson = json.loads(paymentMethod)

    region = "cn-xxx"
    account_id = "xxx"
    ak_id = "xxx"
    ak_secret = "xxx"
    mns_endpoint = "http://your_account_id.mns.cn-hangzhou.aliyuncs.com/"
    queue_name = "payment-queue-fnf-demo"
    my_account = Account(mns_endpoint, ak_id, ak_secret)
    my_queue = my_account.get_queue(queue_name)
    output = "{\"paymentMethod\": \"%s\", \"price\":\"%s\" " \
                         "}" % (paymentMethodJson["paymentMethod"], paymentMethodJson["price"])
    msg = Message(output)
    my_queue.send_message(msg)

    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [HELLO_WORLD]

該函數的邏輯很簡單,就是向 MNS 的隊列 payment-queue-fnf-demo 發送用戶選擇的支付方式和金額。
VUE代碼片段如下:

14.png

8. paymentCombination 節點

paymentCombination 節點是一個路由節點,通過判斷某個參數路由到不同的節點,這裡自然使用 paymentMethod 作為判斷條件。FDL 代碼如下:

- type: choice
    name: paymentCombination
    inputMappings:
      - target: orderNum
        source: $local.orderNum
      - target: paymentMethod
        source: $local.paymentMethod
      - target: price
        source: $local.price
      - target: taskToken
        source: $local.taskToken
    choices:
      - condition: $.paymentMethod == "zhifubao"
        steps:
          - type: task
            name: zhifubao
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
            inputMappings:
              - target: price
                source: $input.price            
              - target: orderNum
                source: $input.orderNum
              - target: paymentMethod
                source: $input.paymentMethod
              - target: taskToken
                source: $input.taskToken
      - condition: $.paymentMethod == "weixin"
        steps:
          - type: task
            name: weixin
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
            inputMappings:
            - target: price
              source: $input.price            
            - target: orderNum
              source: $input.orderNum
            - target: paymentMethod
              source: $input.paymentMethod
            - target: taskToken
              source: $input.taskToken
      - condition: $.paymentMethod == "unionpay"
        steps:
          - type: task
            name: unionpay
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
            inputMappings:
            - target: price
              source: $input.price            
            - target: orderNum
              source: $input.orderNum
            - target: paymentMethod
              source: $input.paymentMethod
            - target: taskToken
              source: $input.taskToken
    default:
      goto: orderCanceled

這裡的流程是,用戶選擇支付方式後,通過消息發送給 payment-fnf-demo 函數,然後將支付方式返回,於是流轉到 paymentCombination 節點通過判斷支付方式流轉到具體處理支付邏輯的節點和函數。

9. zhifubao節點

我們具體來看一個 zhifubao 節點:

    choices:
      - condition: $.paymentMethod == "zhifubao"
        steps:
          - type: task
            name: zhifubao
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
            inputMappings:
              - target: price
                source: $input.price            
              - target: orderNum
                source: $input.orderNum
              - target: paymentMethod
                source: $input.paymentMethod
              - target: taskToken
                source: $input.taskToken

這個節點的 resourceArn 和之前兩個節點的不同,這裡配置的是函數計算中函數的 ARN,也就是說當流程流轉到這個節點時會觸發 zhifubao-fnf-demo 函數,該函數是一個事件觸發函數,但不需要創建任何觸發器。流程將訂單金額、訂單號、支付方式傳給 zhifubao-fnf-demo 函數。

10. zhifubao-fnf-demo 函數

現在我們來看 zhifubao-fnf-demo 函數的代碼:

# -*- coding: utf-8 -*-
import logging
import json
import requests
import urllib.parse
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest

def handler(event, context):
  region = "cn-xxx"
  account_id = "xxx"
  ak_id = "xxx"
  ak_secret = "xxx"
  fnf_client = AcsClient(
    ak_id,
    ak_secret,
    region
  )
  logger = logging.getLogger()
  logger.info(event)
  bodyJson = json.loads(event)
  price = bodyJson["price"]
  taskToken = bodyJson["taskToken"]
  orderNum = bodyJson["orderNum"]
  paymentMethod = bodyJson["paymentMethod"]
  logger.info("price:" + price)
  newPrice = int(price) * 0.8
  logger.info("newPrice:" + str(newPrice))
  url = "http://xx.xx.xx.xx:8080/setPaymentCombination/" + orderNum + "/" + paymentMethod + "/" + str(newPrice)
  x = requests.get(url)

  return {"Status":"ok"}

示例中的代碼邏輯很簡單,接收到金額後,將金額打 8 折,然後將價格更新回訂單。其他支付方式的節點和函數如法炮製,變更實現邏輯就可以。在這個示例中,微信支付打了 5 折,銀聯支付打 7 折。

11. 完整流程

流程中的 orderCompleted 和 orderCanceled 節點沒做什麼邏輯,大家可以自行發揮,思路和之前的節點一樣。所以完整的流程是這樣:

15.png

從 Serverless 工作流中看到的節點流轉是這樣的:

16.png

總結

到此,我們基於 Serverless 工作流和 Serverless 函數計算構建的訂單模塊示例就算完成了,在示例中,有兩個點需要大家注意:

  • 配置商家和支付方式的元數據規則。

  • 確認支付頁面的元數據規則。

因為在實際生產中,我們需要將可定製的部分都抽象為元數據描述,需要有配置界面制定商家的支付方式即更新元數據規則,然後前端頁面基於元數據信息展示相應的內容。

所以如果之後需要接入其他的支付方式,只需在 paymentCombination 路由節點中確定好路由規則,然後增加對應的支付方式函數即可。通過增加元數據配置項,就可以在頁面顯示新加的支付方式,並且路由到處理新支付方式的函數中。

以上內容作為拋磚引玉之石,探索 Serverless 的應用場景,來解決 SaaS 廠商靈活性和擴展性的痛點。大家如果有任何疑問也可以加入釘釘群:35712134 來尋找答案,我們不見不散!

課程推薦

為了更多開發者能夠享受到 Serverless 帶來的紅利,這一次,我們集結了 10+ 位阿里巴巴 Serverless 領域技術專家,打造出最適合開發者入門的 Serverless 公開課,讓你即學即用,輕鬆擁抱雲計算的新範式——Serverless。

點擊即可免費觀看課程:https://developer.aliyun.com/learning/roadmap/serverless

Leave a Reply

Your email address will not be published. Required fields are marked *