随着互联网人口红利逐渐减弱,基于流量的增长已经放缓,互联网行业迫切需要找到一片足以承载自身持续增长的新蓝海,产业互联网正是这一宏大背景下的新趋势。我们看到互联网浪潮正在席卷传统行业,云计算、大数据、人工智能开始大规模融入到金融、制造、物流、零售、文娱、教育、医疗等行业的生产环节中,这种融合称为产业互联网。

a而在产业互联网中,有一块不可小觑的领域是 SaaS 领域,它是 ToB 赛道的中间力量,比如 CRM、HRM、费控系统、财务系统、协同办公等等。

%title插图%num

SaaS 系统面临的挑战

在消费互联网时代,大家是搜索想要的东西,各个厂商在云计算、大数据、人工智能等技术基座之上建立流量*大化的服务与生态,基于海量内容分发与流量共享为逻辑构建系统。而到了产业互联网时代,供给关系发生了变化,大家是定制想要的东西,需要从供给与需求两侧出发进行双向建设,这个时候系统的灵活性和扩展性面临着前所未有的挑战,尤其是 ToB 的 SaaS 领域。

%title插图%num

特别对于当下的经济环境,SaaS 厂商要明白,不能再通过烧钱的方式,只关注在自己的用户数量上,而更多的要思考如何帮助客户降低成本、增加效率,所以需要将更多的精力放在自己产品的定制化能力上。

%title插图%num

%title插图%num

如何应对挑战

SaaS 领域中的佼佼者 Salesforce,将 CRM 的概念扩展到 Marketing、Sales、Service,而这三块领域中只有 Sales 有专门的 SaaS 产品,其他两个领域都是各个 ISV 在不同行业的行业解决方案,靠的是什么?毋庸置疑,是 Salesforce 强大的 aPaaS 平台。ISV、内部实施、客户均可以在各自维度通过 aPaaS 平台构建自己行业、自己领域的 SaaS 系统,建立完整的生态。所以在我看来,现在的 Salesforce 已经由一家 SaaS 公司升华为一家 aPaaS 平台公司了。这种演进的过程也印证了消费互联网和产业互联网的转换逻辑以及后者的核心诉求。

然而不是所有 SaaS 公司都有财力和时间去孵化和打磨自己的 aPaaS 平台,但市场的变化、用户的诉求是实实在在存在的。若要生存,就要求变。这个变的核心就是能够让自己目前的 SaaS 系统变得灵活起来,相对建设困难的 aPaaS 平台,我们其实可以选择轻量且有效的 Serverless 方案来提升现有系统的灵活性和可扩展性,从而实现用户不同的定制需求。

%title插图%num

Serverless工作流

Serverless 工作流是一个用来协调多个分布式任务执行的全托管云服务。在 Serverless工作流中,可以用顺序、分支、并行等方式来编排分布式任务,Serverless 工作流会按照设定好的步骤可靠地协调任务执行,跟踪每个任务的状态转换,并在必要时执行您定义的重试逻辑,以确保工作流顺利完成。Serverless 工作流通过提供日志记录和审计来监视工作流的执行,可以轻松地诊断和调试应用。

下面这张图描述了 Serverless 工作流如何协调分布式任务,这些任务可以是函数、已集成云服务API、运行在虚拟机或容器上的程序。

%title插图%num

看完 Serverless 工作流的介绍,大家可能已经多少有点思路了吧。系统灵活性和可扩展性的核心是服务可编排,无论是以前的BPM还是现在的 aPaaS。所以基于 Serverless 工作流重构SaaS系统灵活性方案的核心思路,是将系统内用户*希望定制的功能进行梳理、拆分、抽离,再配合函数计算(FC)提供无状态的能力,通过 Serverless 工作流进行这些功能点的编排,从而实现不同的业务流程。

%title插图%num

通过函数计算 FC 和 Serverless 工作流搭建灵活的订餐模块

订餐场景相信大家都不会陌生,在家叫外卖或者在餐馆点餐,都涉及到这个场景。当下也有很多提供点餐系统的 SaaS 服务厂商,有很多不错的 SaaS 点餐系统。

随着消费互联网向产业互联网转换,这些 SaaS 点餐系统面临的定制化的需求也越来越多,其中有一个需求是不同的商家在支付时会显示不同的支付方式,比如从A商家点餐后付款时显示支付宝、微信支付、银联支付,从B商家点餐后付款时显示支付宝、京东支付。突然美团又冒出来了美团支付,此时B商家接了美团支付,那么从B商家点餐后付款时显示支付宝、京东支付、美团支付。

诸如此类的定制化需求越来越多,这些 SaaS 产品如果没有 PaaS 平台,那么就会疲于不断的通过硬代码增加条件判断来实现不同商家的需求,这显然不是一个可持续发展的模式。

那么我们来看看通过函数计算 FC 和 Serverless 工作流如何优雅的解决这个问题。先来看看这个点餐流程:

%title插图%num

通过Serverless工作流创建流程

首选我需要将上面用户侧的流程转变为程序侧的流程,此时就需要使用 Serverless 工作流来担任此任务了。

打开 Serverless 控制台,创建订餐流程,这里 Serverless 工作流使用流程定义语言 FDL 创建工作流,如何使用FDL创建工作流请参阅文档。流程图如下图所示:

%title插图%num

FDL 代码为:

  1. 1version: v1beta1
  2. 2type: flow
  3. 3timeoutSeconds: 3600
  4. 4steps:
  5. 5  – type: task
  6. 6    name: generateInfo
  7. 7    timeoutSeconds: 300
  8. 8    resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
  9. 9    pattern: waitForCallback
  10. 10    inputMappings:
  11. 11      – target: taskToken
  12. 12        source: $context.task.token
  13. 13      – target: products
  14. 14        source: $input.products
  15. 15      – target: supplier
  16. 16        source: $input.supplier
  17. 17      – target: address
  18. 18        source: $input.address
  19. 19      – target: orderNum
  20. 20        source: $input.orderNum
  21. 21      – target: type
  22. 22        source: $context.step.name
  23. 23    outputMappings:
  24. 24      – target: paymentcombination
  25. 25        source: $local.paymentcombination
  26. 26      – target: orderNum
  27. 27        source: $local.orderNum
  28. 28    serviceParams:
  29. 29      MessageBody: $
  30. 30      Priority: 1
  31. 31    catch:
  32. 32      – errors:
  33. 33          – FnF.TaskTimeout
  34. 34        goto: orderCanceled
  35. 35  -type: task
  36. 36    name: payment
  37. 37    timeoutSeconds: 300
  38. 38    resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
  39. 39    pattern: waitForCallback
  40. 40    inputMappings:
  41. 41      – target: taskToken
  42. 42        source: $context.task.token
  43. 43      – target: orderNum
  44. 44        source: $local.orderNum
  45. 45      – target: paymentcombination
  46. 46        source: $local.paymentcombination
  47. 47      – target: type
  48. 48        source: $context.step.name
  49. 49    outputMappings:
  50. 50      – target: paymentMethod
  51. 51        source: $local.paymentMethod
  52. 52      – target: orderNum
  53. 53        source: $local.orderNum
  54. 54      – target: price
  55. 55        source: $local.price
  56. 56      – target: taskToken
  57. 57        source: $input.taskToken
  58. 58    serviceParams:
  59. 59      MessageBody: $
  60. 60      Priority: 1
  61. 61    catch:
  62. 62      – errors:
  63. 63          – FnF.TaskTimeout
  64. 64        goto: orderCanceled
  65. 65  – type: choice
  66. 66    name: paymentCombination
  67. 67    inputMappings:
  68. 68      – target: orderNum
  69. 69        source: $local.orderNum
  70. 70      – target: paymentMethod
  71. 71        source: $local.paymentMethod
  72. 72      – target: price
  73. 73        source: $local.price
  74. 74      – target: taskToken
  75. 75        source: $local.taskToken
  76. 76    choices:
  77. 77      – condition: $.paymentMethod == “zhifubao”
  78. 78        steps:
  79. 79          – type: task
  80. 80            name: zhifubao
  81. 81            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
  82. 82            inputMappings:
  83. 83              – target: price
  84. 84                source: $input.price
  85. 85              – target: orderNum
  86. 86                source: $input.orderNum
  87. 87              – target: paymentMethod
  88. 88                source: $input.paymentMethod
  89. 89              – target: taskToken
  90. 90                source: $input.taskToken
  91. 91      – condition: $.paymentMethod == “weixin”
  92. 92        steps:
  93. 93          – type: task
  94. 94            name: weixin
  95. 95            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
  96. 96            inputMappings:
  97. 97            – target: price
  98. 98              source: $input.price
  99. 99            – target: orderNum
  100. 100              source: $input.orderNum
  101. 101            – target: paymentMethod
  102. 102              source: $input.paymentMethod
  103. 103            – target: taskToken
  104. 104              source: $input.taskToken
  105. 105      – condition: $.paymentMethod == “unionpay”
  106. 106        steps:
  107. 107          – type: task
  108. 108            name: unionpay
  109. 109            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
  110. 110            inputMappings:
  111. 111            – target: price
  112. 112              source: $input.price
  113. 113            – target: orderNum
  114. 114              source: $input.orderNum
  115. 115            – target: paymentMethod
  116. 116              source: $input.paymentMethod
  117. 117            – target: taskToken
  118. 118              source: $input.taskToken
  119. 119    default:
  120. 120      goto: orderCanceled
  121. 121  – type: task
  122. 122    name: orderCompleted
  123. 123    resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/orderCompleted
  124. 124    end: true
  125. 125  – type: task
  126. 126    name: orderCanceled
  127. 127    resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/cancerOrde

在解析整个流程之前,我先要说明的一点是,我们不是完全通过 Serverless 函数计算和 Serverless 工作流来搭建订餐模块,只是用它来解决灵活性的问题,所以这个示例的主体应用是Java编写的,然后结合了 Serverless 函数计算和 Serverless 工作流。下面我们来详细解析这个流程。

启动流程

按常理,开始点餐时流程就应该启动了,所以在这个示例中,我的设计是当我们选择完商品和商家、填完地址后启动流程:

%title插图%num

这里我们通过 Serverless 工作流提供的 OpenAPI 来启动流程。

%title插图%num

  • Java 启动流程

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

  1. 1<dependency>
  2. 2<groupId>com.aliyun</groupId>
  3. 3<artifactId>aliyun-java-sdk-core</artifactId>
  4. 4<version>[4.3.2,5.0.0)</version>
  5. 5</dependency>
  6. 6<dependency>
  7. 7<groupId>com.aliyun</groupId>
  8. 8<artifactId>aliyun-java-sdk-fnf</artifactId>
  9. 9<version>[1.0.0,5.0.0)</version>
  10. 10</dependency>

然后创建初始化 Java SDK 的 Config 类:

  1. 1@Configuration
  2. 2public class FNFConfig {
  3. 3
  4. 4@Bean
  5. 5public IAcsClient createDefaultAcsClient(){
  6. 6        DefaultProfile profile = DefaultProfile.getProfile(
  7. 7“cn-xxx”,          // 地域ID
  8. 8“ak”,      // RAM 账号的AccessKey ID
  9. 9“sk”); // RAM 账号Access Key Secret
  10. 10        IAcsClient client = new DefaultAcsClient(profile);
  11. 11return client;
  12. 12    }
  13. 13
  14. 14}

再来看 Controller 中的 startFNF 方法,该方法暴露 GET 方式的接口,传入三个参数:

1、fnfname:要启动的流程名称。

2、execuname:流程启动后的流程实例名称。

3、input:启动输入参数,比如业务参数。

  1. 1@GetMapping(“/startFNF/{fnfname}/{execuname}/{input}”)
  2. 2public StartExecutionResponse startFNF(@PathVariable(“fnfname”String fnfName,
  3. 3@PathVariable(“execuname”String execuName,
  4. 4@PathVariable(“input”String inputStr) throws ClientException {
  5. 5        JSONObject jsonObject = new JSONObject();
  6. 6        jsonObject.put(“fnfname”, fnfName);
  7. 7        jsonObject.put(“execuname”, execuName);
  8. 8        jsonObject.put(“input”, inputStr);
  9. 9return fnfService.startFNF(jsonObject);
  10. 10    }}

再来看 Service 中的 startFNF 方法,该方法分两部分,*个部分是启动流程,第二部分是创建订单对象,并模拟入库(示例中是放在 Map 里了):

  1. 1 @Override
  2. 2public StartExecutionResponse startFNF(JSONObject jsonObject) throws ClientException {
  3. 3        StartExecutionRequest request = new StartExecutionRequest();
  4. 4        String orderNum = jsonObject.getString(“execuname”);
  5. 5        request.setFlowName(jsonObject.getString(“fnfname”));
  6. 6        request.setExecutionName(orderNum);
  7. 7        request.setInput(jsonObject.getString(“input”));
  8. 8
  9. 9        JSONObject inputObj = jsonObject.getJSONObject(“input”);
  10. 10        Order order = new Order();
  11. 11        order.setOrderNum(orderNum);
  12. 12        order.setAddress(inputObj.getString(“address”));
  13. 13        order.setProducts(inputObj.getString(“products”));
  14. 14        order.setSupplier(inputObj.getString(“supplier”));
  15. 15        orderMap.put(orderNum, order);
  16. 16
  17. 17return iAcsClient.getAcsResponse(request);
  18. 18    }

启动流程时,流程名称和启动流程实例的名称是需要传入的参数,这里我将每次的订单编号作为启动流程的实例名称。至于 Input,可以根据需求构造 JSON 字符串传入。这里我将商品、商家、地址、订单号构造了 JSON 字符串在流程启动时传入流程中。

另外,创建了此次订单的 Order 实例,并存在 Map 中,模拟入库,后续环节还会查询该订单实例更新订单属性。

  • VUE 选择商品/商家页面

前端我使用 VUE 搭建,当点击选择商品和商家页面中的下一步后,通过 GET 方式调用 HTTP 协议的接口/startFNF/{fnfname}/{execuname}/{input}。和上面的 Java 方法对应。

1、fnfname:要启动的流程名称。

2、execuname:随机生成 uuid,作为订单的编号,也作为启动流程实例的名称。

3、input:将商品、商家、订单号、地址构建为 JSON 字符串传入流程。

  1. 1            submitOrder(){
  2. 2const orderNum = uuid.v1()
  3. 3this.$axios.$get(‘/startFNF/OrderDemo-Jiyuan/’+orderNum+‘/{\n’ +
  4. 4‘  “products”: “‘+this.products+‘”,\n’ +
  5. 5‘  “supplier”: “‘+this.supplier+‘”,\n’ +
  6. 6‘  “orderNum”: “‘+orderNum+‘”,\n’ +
  7. 7‘  “address”: “‘+this.address+‘”\n’ +
  8. 8‘}’ ).then((response) => {
  9. 9                    console.log(response)
  10. 10if(response.message == “success”){
  11. 11this.$router.push(‘/orderdemo/’ + orderNum)
  12. 12                    }
  13. 13                })
  14. 14            }

generateInfo 节点

*个节点 generateInfo,先来看看 FDL 的含义:

  1. 1 – type: task
  2. 2name: generateInfo
  3. 3timeoutSeconds: 300
  4. 4resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
  5. 5pattern: waitForCallback
  6. 6inputMappings:
  7. 7      – target: taskToken
  8. 8source: $context.task.token
  9. 9      – target: products
  10. 10source: $input.products
  11. 11      – target: supplier
  12. 12source: $input.supplier
  13. 13      – target: address
  14. 14source: $input.address
  15. 15      – target: orderNum
  16. 16source: $input.orderNum
  17. 17      – target: type
  18. 18source: $context.step.name
  19. 19outputMappings:
  20. 20      – target: paymentcombination
  21. 21source: $local.paymentcombination
  22. 22      – target: orderNum
  23. 23source: $local.orderNum
  24. 24serviceParams:
  25. 25MessageBody: $
  26. 26Priority: 1
  27. 27catch:
  28. 28      – errors:
  29. 29          – FnF.TaskTimeout
  30. 30goto: orderCanceled

1、name:节点名称。

2、timeoutSeconds:超时时间。该节点等待的时长,超过时间后会跳转到 goto 分支指向的 orderCanceled 节点。

3、pattern:设置为 waitForCallback,表示需要等待确认。inputMappings:该节点入参。

  • taskToken:Serverless 工作流自动生成的 Token。
  • products:选择的商品。
  • supplier:选择的商家。
  • address:送餐地址。
  • orderNum:订单号。

4、outputMappings:该节点的出参。

  • paymentcombination:该商家支持的支付方式。
  • orderNum:订单号。

5、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:

%title插图%num

创建 MNS 触发器,选择监听 generateInfo-fnf-demo-jiyuanTopic。

%title插图%num

打开消息服务 MNS 控制台,创建 generateInfo-fnf-demo-jiyuanTopic:

%title插图%num

做好函数的准备工作,我们来开始写代码:

  1. 1# -*- coding: utf-8 -*-
  2. 2import logging
  3. 3import json
  4. 4import time
  5. 5import requests
  6. 6from aliyunsdkcore.client import AcsClient
  7. 7from aliyunsdkcore.acs_exception.exceptions import ServerException
  8. 8from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
  9. 9from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
  10. 10
  11. 11
  12. 12def handler(event, context):
  13. 13# 1. 构建Serverless工作流Client
  14. 14    region = “cn-hangzhou”
  15. 15    account_id = “XXXX”
  16. 16    ak_id = “XXX”
  17. 17    ak_secret = “XXX”
  18. 18    fnf_client = AcsClient(
  19. 19        ak_id,
  20. 20        ak_secret,
  21. 21        region
  22. 22    )
  23. 23    logger = logging.getLogger()
  24. 24# 2. event内的信息即接受到Topic generateInfo-fnf-demo-jiyuan中的消息内容,将其转换为Json对象
  25. 25    bodyJson = json.loads(event)
  26. 26    logger.info(“products:” + bodyJson[“products”])
  27. 27    logger.info(“supplier:” + bodyJson[“supplier”])
  28. 28    logger.info(“address:” + bodyJson[“address”])
  29. 29    logger.info(“taskToken:” + bodyJson[“taskToken”])
  30. 30    supplier = bodyJson[“supplier”]
  31. 31    taskToken = bodyJson[“taskToken”]
  32. 32    orderNum = bodyJson[“orderNum”]
  33. 33# 3. 判断什么商家使用什么样的支付方式组合,这里的示例比较简单粗暴,正常情况下,应该使用元数据配置的方式获取
  34. 34    paymentcombination = “”
  35. 35if supplier == “haidilao”:
  36. 36        paymentcombination = “zhifubao,weixin”
  37. 37else:
  38. 38        paymentcombination = “zhifubao,weixin,unionpay”
  39. 39
  40. 40# 4. 调用Java服务暴露的接口,更新订单信息,主要是更新支付方式
  41. 41    url = “http://xx.xx.xx.xx:8080/setPaymentCombination/” + orderNum + “/” + paymentcombination + “/0”
  42. 42    x = requests.get(url)
  43. 43
  44. 44# 5. 给予generateInfo节点响应,并返回数据,这里返回了订单号和支付方式
  45. 45    output = “{\”orderNum\”: \”%s\”, \”paymentcombination\”:\”%s\” “ \
  46. 46“}” % (orderNum, paymentcombination)
  47. 47    request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
  48. 48    request.set_Output(output)
  49. 49    request.set_TaskToken(taskToken)
  50. 50    resp = fnf_client.do_action_with_exception(request)
  51. 51return ‘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 代码:

  1. 1– type: task
  2. 2name: payment
  3. 3timeoutSeconds: 300
  4. 4resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
  5. 5pattern: waitForCallback
  6. 6inputMappings:
  7. 7      – target: taskToken
  8. 8source: $context.task.token
  9. 9      – target: orderNum
  10. 10source: $local.orderNum
  11. 11      – target: paymentcombination
  12. 12source: $local.paymentcombination
  13. 13      – target: type
  14. 14source: $context.step.name
  15. 15outputMappings:
  16. 16      – target: paymentMethod
  17. 17source: $local.paymentMethod
  18. 18      – target: orderNum
  19. 19source: $local.orderNum
  20. 20      – target: price
  21. 21source: $local.price
  22. 22      – target: taskToken
  23. 23source: $input.taskToken
  24. 24serviceParams:
  25. 25MessageBody: $
  26. 26Priority: 1
  27. 27catch:
  28. 28      – errors:
  29. 29          – FnF.TaskTimeout
  30. 30goto: orderCanceled

当流程流转到 payment 节点后,意味着用户进入了支付页面。

%title插图%num

这时 payment 节点会向 MNS 的 Topicpayment-fnf-demo-jiyuan 发送消息,会触发 payment-fnf-demo 函数。

payment-fnf-demo函数

payment-fnf-demo 函数的创建方式和 generateInfo-fnf-demo 函数类似,这里不再累赘。我们直接来看代码:

  1. 1# -*- coding: utf-8 -*-
  2. 2import logging
  3. 3import json
  4. 4import os
  5. 5import time
  6. 6import logging
  7. 7from aliyunsdkcore.client import AcsClient
  8. 8from aliyunsdkcore.acs_exception.exceptions import ServerException
  9. 9from aliyunsdkcore.client import AcsClient
  10. 10from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
  11. 11from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
  12. 12from mns.account import Account  # pip install aliyun-mns
  13. 13from mns.queue import *
  14. 14
  15. 15
  16. 16def handler(event, context):
  17. 17    logger = logging.getLogger()
  18. 18    region = “xxx”
  19. 19    account_id = “xxx”
  20. 20    ak_id = “xxx”
  21. 21    ak_secret = “xxx”
  22. 22    mns_endpoint = “http://your_account_id.mns.cn-hangzhou.aliyuncs.com/”
  23. 23    queue_name = “payment-queue-fnf-demo”
  24. 24    my_account = Account(mns_endpoint, ak_id, ak_secret)
  25. 25    my_queue = my_account.get_queue(queue_name)
  26. 26# my_queue.set_encoding(False)
  27. 27    fnf_client = AcsClient(
  28. 28        ak_id,
  29. 29        ak_secret,
  30. 30        region
  31. 31    )
  32. 32    eventJson = json.loads(event)
  33. 33
  34. 34    isLoop = True
  35. 35while isLoop:
  36. 36try:
  37. 37            recv_msg = my_queue.receive_message(30)
  38. 38            isLoop = False
  39. 39# body = json.loads(recv_msg.message_body)
  40. 40            logger.info(“recv_msg.message_body:======================” + recv_msg.message_body)
  41. 41            msgJson = json.loads(recv_msg.message_body)
  42. 42            my_queue.delete_message(recv_msg.receipt_handle)
  43. 43# orderCode = int(time.time())
  44. 44            task_token = eventJson[“taskToken”]
  45. 45            orderNum = eventJson[“orderNum”]
  46. 46            output = “{\”orderNum\”: \”%s\”, \”paymentMethod\”: \”%s\”, \”price\”: \”%s\” “ \
  47. 47“}” % (orderNum, msgJson[“paymentMethod”], msgJson[“price”])
  48. 48            request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
  49. 49            request.set_Output(output)
  50. 50            request.set_TaskToken(task_token)
  51. 51            resp = fnf_client.do_action_with_exception(request)
  52. 52except Exception as e:
  53. 53            logger.info(“new loop”)
  54. 54return ‘hello world’

该函数的核心思路是等待用户在支付页面选择某个支付方式确认支付。所以这里使用了 MNS 的队列来模拟等待。循环等待接收队列 payment-queue-fnf-demo 中的消息,当收到消息后将订单号和用户选择的具体支付方式以及金额返回给 payment 节点。

VUE选择支付方式页面

因为经过 generateInfo 节点后,该订单的支付方式信息已经有了,所以对于用户而言,当填完商品、商家、地址后,跳转到的页面就是该确认支付页面,并且包含了该商家支持的支付方式。

%title插图%num

当进入该页面后,会请求 Java 服务暴露的接口,获取订单信息,根据支付方式在页面上显示不同的支付方式。代码片段如下:

%title插图%num

当用户选定某个支付方式点击提交订单按钮后,向 payment-queue-fnf-demo 队列发送消息,即通知 payment-fnf-demo 函数继续后续的逻辑。

这里我使用了一个 HTTP 触发器类型的函数,用于实现向 MNS 发消息的逻辑,paymentMethod-fnf-demo 函数代码如下。

  1. 1# -*- coding: utf-8 -*-
  2. 2
  3. 3import logging
  4. 4import urllib.parse
  5. 5import json
  6. 6from mns.account import Account  # pip install aliyun-mns
  7. 7from mns.queue import *
  8. 8HELLO_WORLD = b’Hello world!\n’
  9. 9
  10. 10def handler(environ, start_response):
  11. 11    logger = logging.getLogger()
  12. 12    context = environ[‘fc.context’]
  13. 13    request_uri = environ[‘fc.request_uri’]
  14. 14for k, v in environ.items():
  15. 15if k.startswith(‘HTTP_’):
  16. 16# process custom request headers
  17. 17pass
  18. 18try:
  19. 19        request_body_size = int(environ.get(‘CONTENT_LENGTH’0))
  20. 20except (ValueError):
  21. 21        request_body_size = 0
  22. 22    request_body = environ[‘wsgi.input’].read(request_body_size)
  23. 23    paymentMethod = urllib.parse.unquote(request_body.decode(“GBK”))
  24. 24    logger.info(paymentMethod)
  25. 25    paymentMethodJson = json.loads(paymentMethod)
  26. 26
  27. 27    region = “cn-xxx”
  28. 28    account_id = “xxx”
  29. 29    ak_id = “xxx”
  30. 30    ak_secret = “xxx”
  31. 31    mns_endpoint = “http://your_account_id.mns.cn-hangzhou.aliyuncs.com/”
  32. 32    queue_name = “payment-queue-fnf-demo”
  33. 33    my_account = Account(mns_endpoint, ak_id, ak_secret)
  34. 34    my_queue = my_account.get_queue(queue_name)
  35. 35    output = “{\”paymentMethod\”: \”%s\”, \”price\”:\”%s\” “ \
  36. 36“}” % (paymentMethodJson[“paymentMethod”], paymentMethodJson[“price”])
  37. 37    msg = Message(output)
  38. 38    my_queue.send_message(msg)
  39. 39
  40. 40    status = ‘200 OK’
  41. 41    response_headers = [(‘Content-type’‘text/plain’)]
  42. 42    start_response(status, response_headers)
  43. 43return [HELLO_WORLD]

该函数的逻辑很简单,就是向 MNS 的队列 payment-queue-fnf-demo 发送用户选择的支付方式和金额。

VUE代码片段如下:

%title插图%num

paymentCombination 节点

paymentCombination 节点是一个路由节点,通过判断某个参数路由到不同的节点,这里自然使用 paymentMethod 作为判断条件。FDL 代码如下:

  1. 1– type: choice
  2. 2    name: paymentCombination
  3. 3    inputMappings:
  4. 4      – target: orderNum
  5. 5source: $local.orderNum
  6. 6      – target: paymentMethod
  7. 7source: $local.paymentMethod
  8. 8      – target: price
  9. 9source: $local.price
  10. 10      – target: taskToken
  11. 11source: $local.taskToken
  12. 12    choices:
  13. 13      – condition: $.paymentMethod == “zhifubao”
  14. 14        steps:
  15. 15          – type: task
  16. 16            name: zhifubao
  17. 17            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
  18. 18            inputMappings:
  19. 19              – target: price
  20. 20source: $input.price
  21. 21              – target: orderNum
  22. 22source: $input.orderNum
  23. 23              – target: paymentMethod
  24. 24source: $input.paymentMethod
  25. 25              – target: taskToken
  26. 26source: $input.taskToken
  27. 27      – condition: $.paymentMethod == “weixin”
  28. 28        steps:
  29. 29          – type: task
  30. 30            name: weixin
  31. 31            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
  32. 32            inputMappings:
  33. 33            – target: price
  34. 34source: $input.price
  35. 35            – target: orderNum
  36. 36source: $input.orderNum
  37. 37            – target: paymentMethod
  38. 38source: $input.paymentMethod
  39. 39            – target: taskToken
  40. 40source: $input.taskToken
  41. 41      – condition: $.paymentMethod == “unionpay”
  42. 42        steps:
  43. 43          – type: task
  44. 44            name: unionpay
  45. 45            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
  46. 46            inputMappings:
  47. 47            – target: price
  48. 48source: $input.price
  49. 49            – target: orderNum
  50. 50source: $input.orderNum
  51. 51            – target: paymentMethod
  52. 52source: $input.paymentMethod
  53. 53            – target: taskToken
  54. 54source: $input.taskToken
  55. 55    default:
  56. 56      goto: orderCanceled

这里的流程是,用户选择支付方式后,通过消息发送给 payment-fnf-demo 函数,然后将支付方式返回,于是流转到 paymentCombination 节点通过判断支付方式流转到具体处理支付逻辑的节点和函数。

zhifubao节点

我们具体来看一个 zhifubao 节点:

  1. 1choices:
  2. 2      – condition: $.paymentMethod == “zhifubao”
  3. 3        steps:
  4. 4          – type: task
  5. 5            name: zhifubao
  6. 6            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
  7. 7            inputMappings:
  8. 8              – target: price
  9. 9source: $input.price
  10. 10              – target: orderNum
  11. 11source: $input.orderNum
  12. 12              – target: paymentMethod
  13. 13source: $input.paymentMethod
  14. 14              – target: taskToken
  15. 15source: $input.taskToken

这个节点的 resourceArn 和之前两个节点的不同,这里配置的是函数计算中函数的 ARN,也就是说当流程流转到这个节点时会触发 zhifubao-fnf-demo 函数,该函数是一个事件触发函数,但不需要创建任何触发器。流程将订单金额、订单号、支付方式传给 zhifubao-fnf-demo 函数。

zhifubao-fnf-demo函数

现在我们来看zhifubao-fnf-demo函数的代码:

  1. 1# -*- coding: utf-8 -*-
  2. 2import logging
  3. 3import json
  4. 4import requests
  5. 5import urllib.parse
  6. 6from aliyunsdkcore.client import AcsClient
  7. 7from aliyunsdkcore.acs_exception.exceptions import ServerException
  8. 8from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
  9. 9from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
  10. 10
  11. 11
  12. 12def handler(event, context):
  13. 13  region = “cn-xxx”
  14. 14  account_id = “xxx”
  15. 15  ak_id = “xxx”
  16. 16  ak_secret = “xxx”
  17. 17  fnf_client = AcsClient(
  18. 18    ak_id,
  19. 19    ak_secret,
  20. 20    region
  21. 21  )
  22. 22  logger = logging.getLogger()
  23. 23  logger.info(event)
  24. 24  bodyJson = json.loads(event)
  25. 25  price = bodyJson[“price”]
  26. 26  taskToken = bodyJson[“taskToken”]
  27. 27  orderNum = bodyJson[“orderNum”]
  28. 28  paymentMethod = bodyJson[“paymentMethod”]
  29. 29  logger.info(“price:” + price)
  30. 30  newPrice = int(price) * 0.8
  31. 31  logger.info(“newPrice:” + str(newPrice))
  32. 32  url = “http://xx.xx.xx.xx:8080/setPaymentCombination/” + orderNum + “/” + paymentMethod + “/” + str(newPrice)
  33. 33  x = requests.get(url)
  34. 34
  35. 35return {“Status”:“ok”}

示例中的代码逻辑很简单,接收到金额后,将金额打8折,然后将价格更新回订单。其他支付方式的节点和函数如法炮制,变更实现逻辑就可以。在这个示例中,微信支付打了5折,银联支付打7折。

完整流程

流程中的 orderCompleted 和 orderCanceled 节点没做什么逻辑,大家可以自行发挥,思路和之前的节点一样。所以完整的流程是这样:

%title插图%num

从Serverless工作流中看到的节点流转是这样的:

%title插图%num

%title插图%num

总结

到此,我们基于 Serverless 工作流和 Serverless 函数计算构建的订单模块示例就算完成了,在示例中,有两个点需要大家注意:

1. 配置商家和支付方式的元数据规则。

2. 确认支付页面的元数据规则。

因为在实际生产中,我们需要将可定制的部分都抽象为元数据描述,需要有配置界面制定商家的支付方式即更新元数据规则,然后前端页面基于元数据信息展示相应的内容。

所以如果之后需要接入其他的支付方式,只需在 paymentCombination 路由节点中确定好路由规则,然后增加对应的支付方式函数即可。通过增加元数据配置项,就可以在页面显示新加的支付方式,并且路由到处理新支付方式的函数中。