8.3.2 收集顾客信息

如果你曾经订购过披萨,你可能会知道流程。他们首先会询问你的电话号码。电话号码除了能够让送货司机在找不到你家的时候打电话给你,还可以作为你在这个披萨店的标识。如果你是回头客,他们可以使用这个电话号码来查找你的地址,这样他们就知道将你的订单派送到什么地方了。

对于一个新的顾客来讲,查询电话号码不会有什么结果。所以接下来,他们将询问你的地址。这样,披萨店的人就会知道你是谁以及将披萨送到哪里。但是在问你要哪种披萨之前,他们要确认你的地址在他们的配送范围之内。如果不在的话,你需要自己到店里并取走披 萨。

在每个披萨订单开始前的提问和回答阶段可以用图 8.3 的流程图来表示。

这个流程比整体的披萨流程更有意思。这个流程不是线性的而是在好几个地方根据不同的条件有了分支。例如,在查找顾客后,流程可能结束(如果找到了顾客),也有可能转移到注册表单(如果没有找到顾客)。同样,在 check-DeliveryArea 状态,顾客有可能会被警告也有可能不被警告他们的地址在配送范围之外。

以下的程序清单展示了识别顾客的流程定义。

程序清单 8.4 使用 Web 流程来识别饥饿的披萨顾客
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/webflow 
  http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

    <input name="order" required="true"/>
    
    <!-- Customer -->
    <view-state id="welcome">
        <transition on="phoneEntered" to="lookupCustomer"/>
        <transition on="cancel" to="cancel"/>
    </view-state>
    
    <action-state id="lookupCustomer">
        <evaluate result="order.customer" expression=
            "pizzaFlowActions.lookupCustomer(requestParameters.phoneNumber)" />
        <transition to="registrationForm" on-exception=
            "com.springinaction.pizza.service.CustomerNotFoundException" />
        <transition to="customerReady" />
    </action-state>
    
    <view-state id="registrationForm" model="order" popup="true" >
        <on-entry>
          <evaluate expression=
              "order.customer.phoneNumber = requestParameters.phoneNumber" />
        </on-entry>
        <transition on="submit" to="checkDeliveryArea" />
        <transition on="cancel" to="cancel" />
    </view-state>
    
    <decision-state id="checkDeliveryArea">
      <if test="pizzaFlowActions.checkDeliveryArea(order.customer.zipCode)" 
          then="addCustomer" 
          else="deliveryWarning"/>
    </decision-state>
    
    <view-state id="deliveryWarning">
        <transition on="accept" to="addCustomer" />
        <transition on="cancel" to="cancel" />
    </view-state>
    
    <action-state id="addCustomer">
        <evaluate expression="pizzaFlowActions.addCustomer(order.customer)" />
        <transition to="customerReady" />
    </action-state>
            
    <!-- End state -->
    <end-state id="cancel" />
    <end-state id="customerReady">
      <output name="customer" />
    </end-state>
    <global-transitions>
      <transition on="cancel" to="cancel" />
    </global-transitions>
</flow>

这个流程包含了几个新的技巧,包括我们首次使用的 <decision-state> 元素。因为它是 pizza 流程的子流程,所以它也可以接受 Order 对象作为输入。

与前面一样,我们还是将这个流程的定义分解成一个个的状态,让我们从 welcome 状态开始。

询问电话号码

welcome 状态是一个很简单的视图状态,它欢迎访问 Spizza 站点的顾客并要求他们输入电话号码。这个状态并没有什么特殊的。它有两个转移:如果从视图触发 phoneEntered 事件的话,转移会将流程定向到 lookupCustomer,另外一个就是在全局转移中定义的用来响应 cancel 事件的 cancel 转移。

welcome 状态的有趣之处在于视图本身。视图 welcome 定义在 /WEBINF/flows/pizza/customer/welcome.jspx 中,如下所示。

程序清单 8.5 欢迎用户并询问他们的电话号码
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>

  <head><title>Spring Pizza</title></head>

  <body>
    <h2>Welcome to Spring Pizza!!!</h2>
	
		<form:form>
      <input type="hidden" name="_flowExecutionKey" 
             value="${flowExecutionKey}"/>
		  <input type="text" name="phoneNumber"/><br/>
      <input type="submit" name="_eventId_phoneEntered" value="Lookup Customer" />
		</form:form>
	</body>
</html>

这个简单的表单提示用户输入其电话号码。但是表单中有两个特殊的部分来驱动流程继续。

首先要注意的是隐藏的 flowExecutionKey 输入域。当进入视图状态时,流程暂停并等待用户采取一些行为。赋予视图的流程执行 key(flow execution key)就是一种返回流程的“回程票”(claim ticket)。当用户提交表单时,流程执行 key 会 在 _flowExecutionKey 输入域中返回并在流程暂停的位置进行恢复。

还要注意的是提交按钮的名字。按钮名字的 _eventId 部分是提供给 Spring Web Flow 的一个线索,它表明了接下来要触发事件。当点击这个按钮提交表单时,会触发 phoneEntered 事件进而转移到 lookupCustomer。

查找顾客

当欢迎表单提交后,顾客的电话号码将包含在请求参数中并准备用于查询顾客。lookupCustomer 状态的 <evaluate> 元素是查找发生的地方。它将电话号码从请求参数中抽取出来并传递到 pizzaFlowActions bean 的 lookup-Customer() 方法中。

目前,lookupCustomer() 的实现并不重要。只需知道它要么返回 Customer 对象,要么抛出 CustomerNotFoundException 异常。

在前一种情况下,Customer 对象将会设置到 customer 变量中(通过 result 属性)并且默认的转移将把流程带到 customerReady 状态。但是如果不能找到顾客的话,将抛出 CustomerNotFoundException 并且流程被转移到 registrationForm 状态。

注册新顾客

registrationForm 状态是要求用户填写配送地址的。就像我们之前看到的其他视图状态,它将被渲染成 JSP。JSP 文件如下所示。

程序清单 8.6 注册新顾客
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>

  <head><title>Spring Pizza</title></head>

  <body>
    <h2>Customer Registration</h2>
    
    <form:form commandName="order">
      <input type="hidden" name="_flowExecutionKey" 
             value="${flowExecutionKey}"/>
      <b>Phone number: </b><form:input path="customer.phoneNumber"/><br/>
      <b>Name: </b><form:input path="customer.name"/><br/>
      <b>Address: </b><form:input path="customer.address"/><br/>
      <b>City: </b><form:input path="customer.city"/><br/>
      <b>State: </b><form:input path="customer.state"/><br/>
      <b>Zip Code: </b><form:input path="customer.zipCode"/><br/>
      <input type="submit" name="_eventId_submit" 
             value="Submit" />
      <input type="submit" name="_eventId_cancel" 
             value="Cancel" />
    </form:form>
	</body>
</html>

这并非我们在流程中看到的第一个表单。welcome 视图状态也为顾客展现了一个表单,那个表单很简单,并且只有一个输入域,从请求参数中获得输入域的值也很简单。但是注册表单就比较复杂了。

在这里不是通过请求参数一个个地处理输入域,而是以更好的方式将表单绑定到 Customer 对象上 —— 让框架来做所有繁杂的工作。

检查配送区域

在顾客提供其地址后,我们需要确认他的住址在配送范围之内。如果 Spizza 不能派送给他们,那么我们要让顾客知道并建议他们自己到店面里取走披萨。

为了做出这个判断,我们使用了决策状态。决策状态 checkDeliveryArea 有一个元素,它将顾客的邮政编码传递到 pizzaFlowActions bean 的check-DeliveryArea() 方法中。这个方法将会返回一个 Boolean 值:如果顾客在配送区域内则为 true,否则为 false。

如果顾客在配送区域内的话,那流程转移到 addCustomer 状态。否则,顾客被带入到 deliveryWarning 视图状态。deliveryWarning 背后的视图就是/WEB-INF/flows/pizza/customer/deliveryWarning.jspx,如下所示:

程序清单 8.7 告知顾客不能将披萨配送到他们的地址
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
  <head><title>Spring Pizza</title></head>
  
  <body>
		<h2>Delivery Unavailable</h2>
		
		<p>The address is outside of our delivery area. The order
		may still be taken for carry-out.</p>
		
		<a href="${flowExecutionUrl}&_eventId=accept">Accept</a> | 
		<a href="${flowExecutionUrl}&_eventId=cancel">Cancel</a>
  </body>
</html>

在 deliveryWarning.jspx 中与流程相关的两个关键点就是那两个链接,它们允许用户继续订单或者将其取消。通过使用与 welcome 状态相同的 flow-ExecurtionUrl 变量,这些链接分别触发流程中的 accept 或 cancel 事件。如果发送的是 accept 事件,那么流程会转移到 addCustomer 状态。否则,接下来会是全局的取消转移,子流程将会转移到 cancel 结束状态。

稍后我们将介绍结束状态。让我们先来看看 addCustomer 状态。

存储顾客数据

当流程抵达 addCustomer 状态时,用户已经输入了他们的地址。为了将来使用,这个地址需要以某种方式存储起来(可能会存储在数据库中)。add-Customer 状态有一个 <evaluate> 元素,它会调用 pizzaFlowActions bean 的 addCustomer() 方法,并将 customer 流程参数传递进去。

一旦这个过程完成,会执行默认的转移,流程将会转移到 ID 为 customer-Ready 的结束状态。

结束流程

一般来讲,流程的结束状态并不会那么有意思。但是这个流程中,它不仅仅只有一个结束状态,而是两个。当子流程完成时,它会触发一个与结束状态 ID 相同的流程事件。如果流程只有一个结束状态的话,那么它始终会触发相同的事件。但是如果有两个或更多的结束状态,流程能够影响到调用状态的执行方向。

当 customer 流程走完所有正常的路径后,它最终会到达 ID 为 customer-Ready 的结束状态。当调用它的披萨流程恢复时,它会接收到一个customer-Ready 事件,这个事件将使得流程转移到 buildOrder 状态。

要注意的是 customerReady 结束状态包含了一个 <output> 元素。在流程中这个元素等同于 Java 中的 return 语句。它从子流程中传递一些数据到调用流程。在本示例中,<output> 元素返回 customer 流程变量,这样在披萨流程中,就能够将 identifyCustomer 子流程的状态指定给订单。另一方面,如果在识别顾客流程的任意地方触发了 cancel 事件,将会通过 ID 为 cancel 的结束状态退出流程,这也会在披萨流程中触发 cancel 事件并导致转移(通过全局转移)到披萨流程的结束状态。

Last updated