Serverless 在 SaaS 領域的最佳實踐

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

SaaS系統面臨的挑戰

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

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

如何應對挑戰

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

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

Serverless工做流

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

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

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

看完Serverless工做流的介紹,你們可能已經多少有點思路了吧。系統靈活性和可擴展性的核心是服務可編排,不管是之前的BPM仍是如今的aPaaS。因此基於Serverless工做流重構SaaS系統靈活性方案的核心思路,是將系統內用戶最但願定製的功能進行梳理、拆分、抽離,再配合函數計算(FC)提供無狀態的能力,經過Serverless工做流進行這些功能點的編排,從而實現不一樣的業務流程。app

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

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

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

經過Serverless工做流建立流程

首先我須要將上面用戶側的流程轉變爲程序側的流程,此時就須要使用Serverless工做流來擔任此任務了。

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

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工做流。下面咱們來詳細解析這個流程。

啓動流程

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

這裏咱們經過Serverless工做流提供的OpenAPI來啓動流程。

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方式的接口,傳入三個參數:

  1. fnfname:要啓動的流程名稱。
  2. execuname:流程啓動後的流程實例名稱。
  3. 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方法對應。

  1. fnfname:要啓動的流程名稱。
  2. execuname:隨機生成uuid,做爲訂單的編號,也做爲啓動流程實例的名稱。
  3. 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)
                    }
                })
            }

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
  1. name:節點名稱。
  2. timeoutSeconds:超時時間。該節點等待的時長,超過期間後會跳轉到goto分支指向的orderCanceled節點。
  3. pattern:設置爲waitForCallback,表示須要等待確認。inputMappings:該節點入參。
  • taskToken:Serverless工做流自動生成的Token。
  • products:選擇的商品。
  • supplier:選擇的商家。
  • address:送餐地址。
  • orderNum:訂單號。
  1. outputMappings:該節點的出參。
  • paymentcombination:該商家支持的支付方式。
  • orderNum:訂單號。
  1. 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發送消息來傳遞,也是使用比較普遍的方式之一。

generateInfo-fnf-demo函數

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

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

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

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

# -*- 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函數。

整個代碼分五部分:

  1. 構建Serverless工做流Client。
  2. event內的信息即接受到TopicgenerateInfo-fnf-demo-jiyuan中的消息內容,將其轉換爲Json對象。
  3. 判斷什麼商家使用什麼樣的支付方式組合,這裏的示例比較簡單粗暴,正常狀況下,應該使用元數據配置的方式獲取。好比在系統內有商家信息的配置功能,經過在界面上配置該商家支持哪些支付方式,造成元數據配置信息,提供查詢接口,在這裏進行查詢。
  4. 調用Java服務暴露的接口,更新訂單信息,主要是更新支付方式。
  5. 給予generateInfo節點響應,並返回數據,這裏返回了訂單號和支付方式。由於該節點的pattern是waitForCallback,因此須要等待響應結果。

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節點後,意味着用戶進入了支付頁面。

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

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節點。

VUE選擇支付方式頁面

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

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

當用戶選定某個支付方式點擊提交訂單按鈕後,向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代碼片斷以下:

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節點經過判斷支付方式流轉到具體處理支付邏輯的節點和函數。

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函數。

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折。

完整流程

流程中的orderCompleted和orderCanceled節點沒作什麼邏輯,你們能夠自行發揮,思路和以前的節點同樣。因此完整的流程是這樣:

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

總結

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

  1. 配置商家和支付方式的元數據規則。
  2. 確認支付頁面的元數據規則。

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

因此若是以後須要接入其餘的支付方式,只需在paymentCombination路由節點中肯定好路由規則,而後增長對應的支付方式函數便可。經過增長元數據配置項,就能夠在頁面顯示新加的支付方式,而且路由處處理新支付方式的函數中。

做者:阿里雲解決方案架構師 計緣

 

原文連接

本文爲阿里雲原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索