jBPM jPDL 3.2用户指南:第9章流程建模

 

作者:JBossWeek http://blog.csdn.net/JBossweek email:jbossweek AT gmail.com

版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息

 

9.1. 概述

流程定义是业务流程的形式化描述。流程定义基于有向图,由节点和转换组成。每个节点都属于某个特定的节点类型,节点类型定义了节点的运行时行为。流程定义有且只有一个开始状态。

令牌是运行时概念,表示执行的一条路线,维护着指向流程图当前节点的指针。

流程实例是流程定义的一次执行。在流程实例被创建的同时,表示执行主路线的令牌也被创建。该令牌被称为流程实例的根令牌。它停留在流程定义的开始状态。

令牌按照信号指令继续流程图的执行。当收到未命名的信号时,令牌将通过缺省的离开转换离开节点。当收到指定转换名称的信号时,令牌将通过指定的转换离开节点。流程实例接收的信号被委托给根令牌。

令牌进入节点以后,节点被执行。节点自身通过让令牌离开实现流程图的继续执行。每种节点类型可以以不同的方式实现流程的继续执行。不能传递执行的节点表示状态。

动作是在流程执行过程中事件发生时被执行的java代码片断。流程图是软件需求交流的重要工具,但只是在建软件的一个视图(投影),它隐藏了许多技术细节。动作是在图形表示的基础上添加技术细节的机制。一旦流程图就绪,就可以添加动作修饰。主要的事件类型有:进入节点、离开节点和执行转换。

9.2. 流程图

流程定义的基础是由节点和转换组成的流程图。这些信息在processdefinition.xml文件中描述。每个节点都有相应的类型,例如state, decision, fork, join。每个节点都有一组离开转换。转换之间可以通过指定名称区分。例如下面的流程图表示jBAY拍卖流程。

The auction process graph

Figure 9.1. The auction process graph

下面是jBAY拍卖流程图的xml表示:

<process-definition>

   
   
    
     
   
   
  <start-state>
    <transition to="auction" />
  </start-state>
  
  <state name="auction">
    <transition name="auction ends" to="salefork" />
    <transition name="cancel" to="end" />
  </state>
  
  <fork name="salefork">
    <transition name="shipping" to="send item" />
    <transition name="billing" to="receive money" />
  </fork>
  
  <state name="send item">
    <transition to="receive item" />
  </state>

   
   
    
     
   
   
  <state name="receive item">
    <transition to="salejoin" />
  </state>
  
  <state name="receive money">
    <transition to="send money" />
  </state>

   
   
    
     
   
   
  <state name="send money">
    <transition to="salejoin" />
  </state>
  
  <join name="salejoin">
    <transition to="end" />
  </join>
  
  <end-state name="end" />
  
</process-definition>

9.3.节点

流程图由节点和转换组成。关于图形及其执行模型的更多信息,请参看Chapter 4, Graph Oriented Programming

每个节点都有相应的类型。节点类型确定了运行时执行到达时节点的行为。jBPM提供了一组预先实现可以使用的节点类型。作为选择,你也可以自己编写定制代码实现某些特定的节点行为。

9.3.1. 节点的职责

每个节点都有两个主要的职责:首先是负责执行与结点功能相关的普通java代码。例如创建一些任务实例,发送通知,更新数据库等等。其次是负责传递流程的执行。基本上,每个节点在传递流程执行时有以下几种可选方式:

·         不传递执行。这种情况下节点表现为等待状态

·         经由节点的某个离开转换传递执行。这就是说最初到达节点的令牌通过API调用executionContext.leaveNode(String)经由某个离开转换被传递。从某种意义上说,可以把这种节点看作自动节点,因为它执行一些客户化的编程逻辑后,不是进行等待,而是自动继续流程的执行。

·         创建新的执行路径。节点可以决定创建新的令牌。每个新令牌都代表一条新的执行路线。经过节点的离开转换后每个新令牌被启动。这种传递方式的一个好的例子是fork节点。

·         结束执行路径。节点可以决定结束执行路径。也就是说令牌结束了,执行路线终止。

·         更一般的情况,节点修改流程实例的整个运行时结构。运行时结构是包含令牌树的流程实例。每个令牌表示一条执行路线。节点可以创建,结束令牌,也可以将令牌设置在流程图的某个节点上,还可以经由转换启动令牌。

与其它工作流和BPM一样,jBPM包含了一组预先实现的节点类型,这些节点类型有确定的文档化配置和行为。但是jBPMGraph Oriented Programming foundation还有一个独特之处,就是向开发人员开放了节点模型。开发人员可以非常方便地编写自己的节点,并在流程中使用。

这是传统的工作流和BPM系统比较封闭的地方。它们通常提供一组固定的节点类型(称为流程语言)。它们的流程语言是封闭的,而且执行模型隐藏在运行环境里。工作流模式的研究表明任意的流程语言都不可能足够强大。因此我们决定采用一个简单的模型,并且允许开发人员编写自己的节点类型,这样就能保证JPDL流程语言的开放性。

接下来,我们将讨论JPDL中最重要的节点类型。

9.3.2. task-node节点类型

任务节点表示一个或者多个由人员执行的任务。因此当执行到达任务节点时,就会在工作流参与人员的任务列表中创建任务实例。这之后,节点表现为等待状态。用户执行完任务后,将触发流程继续执行。也就是说任务完成时令牌上的signal方法将被调用。

9.3.3. state节点类型

状态节点是纯粹的等待状态,它与任务节点的区别是它不会在任何流程参与者的任务列表中创建任务实例。状态节点适用于流程需要等待外部系统的情况。例如,在节点的入口(通过“进入节点”事件的动作),一条消息被发送给外部系统。这之后,流程进入等待状态。当外部系统发送一条响应消息时,token.signal()被调用来触发流程继续执行。

9.3.4. decision节点类型

实际上有两种对决策进行建模的方法。区别在于“谁”做出决策,是流程做出决策,还是由外部的实体提供决策的结果

如果由流程来决策,则应该使用决策节点。基本上有两种设置决策准则的方法。最简单的方法就是在转换上添加condition元素。condition元素是返回boolean值的beanshell 脚本表达式。运行时,决策节点会循环所有的离开转换,计算每个条件,第一个结果为“true”的离开转换将用来传递执行。另一种设置决策准则的方法就是指定DecisionHandler的实现。这样决策的计算在实现DecisionHandlerjava类里完成,然后通过decide方法返回选中的离开转换。

如果由外部实体负责决策(也就是说:不是流程的一部分),应该在状态节点或者等待状态节点上使用多个离开转换。这样在等待状态结束时,就可以通过在外部触发器中指定离开转换来继续执行。例如:Token.signal(String transitionName) TaskInstance.end(String transitionName)

9.3.5. fork节点类型

Fork节点将一条执行路线拆分成多条并行的执行路线。Fork节点缺省的行为是为离开fork节点的每个转换创建一个子令牌,并在子令牌和进入fork节点的令牌之间创建父-子关系。

9.3.6. join节点类型

缺省的join节点假设所有进入的令牌都是同一父亲的子令牌。在使用上面提到fork节点的流程中,当fork节点创建的所有令牌进入同一join节点时就会出现这种情况。Join节点将结束所有进入的令牌,然后检查所有进入令牌的父-子关系。当所有的兄弟令牌都已经进入join节点,父令牌将通过离开转换(唯一)继续传递。如果还有激活的兄弟令牌,join节点表现为一个等待状态。

9.3.7. node节点类型

Node节点用于需要在节点里编写自己代码的情况。Node节点需要一个动作子元素。当执行进入节点时,该动作被执行。在actionhandler中编写的代码可以完成所需的各种工作,同时还要负责执行的传递。

node节点适合需要使用java API实现一些对业务分析师非常重要的功能逻辑的情况。Node节点在流程的图形表示中是可见的。作为比较,下面要介绍的action结点虽然也允许通过代码添加功能逻辑,但是它在流程的图形表示中是不可见的,Action里的功能逻辑对业务分析师来说不重要。

9.4. 转换

转换有一个源节点和一个目的节点。源节点用于from属性表示,目的节点用to属性表示。

 

转换可以根据需要添加名称。值得注意的是,大部分Jbpm特性都依赖于转换名称的唯一性。如果多个转换使用相同的名称,那么其中的第一个将被执行。在代码中遇到名称重复的多个转换时,Map getLeavingTransitionsMap()方法返回的元素将比List getLeavingTransitions()少。

 

缺省的转换是列表中的第一个。

 

9.5. 动作

动作是在流程执行过程中事件发生时被执行的java代码片断。流程图是软件需求交流的重要工具,但只是在建软件的一个视图(投影),它隐藏了许多技术细节。动作是在图形表示的基础上添加技术细节的机制。一旦流程图就绪,就可以添加动作修饰。主要的事件类型有:进入节点、离开节点和执行转换。这说明在不改变图形结构的情况下,可以将java代码与流程图关联。主要的事件类型包括:进入节点、离开节点、执行转换。

值得注意的是事件中的动作和节点中动作的区别。事件中的动作在事件发生时执行。事件上的动作与观察者模式类似,没有办法影响流程的控制流。相反,节点上的动作将负责执行的传递。

让我们来看一个事件上的动作。假设我们需要在指定的转换上更新数据库。虽然从技术角度看数据库的更新非常重要,但是对于业务分析师来说却无关紧要。

A database update action

Figure 9.2. A database update action

public class RemoveEmployeeUpdate implements ActionHandler {
  public void execute(ExecutionContext ctx) throws Exception {
    // get the fired employee from the process variables.
    String firedEmployee = (String) ctx.getContextInstance().getVariable("fired employee");
    
    // by taking the same database connection as used for the jbpm updates, we 
    // reuse the jbpm transaction for our database update.
    Connection connection = ctx.getProcessInstance().getJbpmSession().getSession().getConnection();
    Statement statement = connection.createStatement();
    statement.execute("DELETE FROM EMPLOYEE WHERE ...");
    statement.execute(); 
    statement.close();
  }
}
<process-definition name="yearly evaluation">

   
   
    
     
   
   
  ...
  <state name="fire employee">
    <transition to="collect badge">
      <action class="com.nomercy.hr.RemoveEmployeeUpdate" />
    </transition>
  </state>
  
  <state name="collect badge">
  ...
  
</process-definition>

9.5.1. 动作配置

关于为定制Action添加配置信息以及如何在processdefinition.xml中指定配置的更详细信息,请参看Section 18.2.3, “Configuration of delegations”

9.5.2. 动作引用

Action可以设置名称。命名的Action可以在其它需要设置Action的地方引用。命名的动作也可以作为子元素放在流程定义里。

该特性在希望减少Action配置信息的重复时非常有用(例如:当动作有复杂的配置信息时)。另一个应用场景是运行时动作的执行或调度。

9.5.3. 事件

事件是指流程执行中的某个时刻。jBPM引擎在图形的执行过程中将触发事件,通常发生在计算下一个状态(请阅读:信号的处理)时。事件总是与流程定义中的元素关联,例如流程定义、节点或者转换。大部分的流程元素都可以触发多种类型的事件。例如节点可以触发node-enter事件和node-leave事件。事件是动作的钩子。每个事件都有一个动作列表。当jBPM引擎触发事件时,动作列表将被执行。

9.5.4. 事件传播

Supersate在流程定义的元素之间创建父-子关系。Superstate中的节点和转换将把Superstate作为父节点。顶层的流程元素将把流程定义作为父节点。流程定义没有父节点。当事件被触发时,事件将会沿着父节点的层次进行传播。这为捕获流程中所有的转换事件,并在同一位置集中将这些事件与动作关联提供可能。

9.5.5. 脚本

Script是执行beanshell脚本的动作。关于beanshell的更详细信息,请参看the beanshell website。缺省情况下所有流程变量都可以用于脚本变量,但是脚本变量将不会写回到流程变量。下面的变量也可以在脚本中使用:

·         executionContext

·         token

·         node

·         task

·         taskInstance

<process-definition>
  <event type="node-enter">
    <script>
      System.out.println("this script is entering node "+node);
    </script>
  </event>
  ...
</process-definition>

Variable作为script的子元素,用于定制加载和存储脚本变量的缺省行为,同时还会用到脚本的另一个子元素脚本表达式:expression

<process-definition>
  <event type="process-end">
    <script>
      <expression>
        a = b + c;
      </expression>
      <variable name='XXX' access='write' mapped-name='a' />
      <variable name='YYY' access='read' mapped-name='b' />
      <variable name='ZZZ' access='read' mapped-name='c' />
    </script>
  </event>
  ...
</process-definition>

在脚本开始之前,流程变量YYYZZZ各自加载为脚本变量bc。脚本执行结束之后,脚本变量a的值存储回流程变量XXX

如果变量的访问属性包含‘read’,那么在脚本计算之前流程变量将作为脚本变量加载。如果访问属性包含‘write’,那么在脚本计算之后该脚本变量将存储回流程变量。Mapped-name属性可以让流程变量在脚本中使用另一个名称。当流程变量名称包含空格或者其他非法的脚本字符时,这种名称的映射机制将非常有用。

9.5.6.定制事件

注意在流程执行的过程中,定制事件可以随意触发。事件由流程图元素(节点、转换、流程定义和superstate都是图元素)和事件类型(java.lang.String)的组合唯一确定。jBPM定义了一组节点、转换和其它图形元素可以激活的事件。但是作为用户,在动作中、自定义的节点实现中或者甚至在流程实例之外,你都可以调用GraphElement.fireEvent(String eventType, ExecutionContext executionContext) 随意地激发自己的事件,而且事件类型的名称可以自由选择。

9.6. Superstates

Superstate是一组节点,可以递归嵌套。Superstates的作用在于能够为流程定义添加一定的层次。例如,应用可以将流程中的所有节点按照阶段进行分组。动作可以关联到superstate的事件上,这样在给定的时间点令牌就会驻留在多个嵌套的节点上。这对检查流程的执行例如是否在启动阶段非常方便。在jBPM模型里,你可以自由地将任意的节点组合放在一个superstate中。

9.6.1. Superstate 转换

转换可以离开superstatesuperstate中节点上的令牌可以使用所有离开superstate的转换。转换也可以进入superstate,这时令牌将直接重定向到superstate的第一个节点。Superstate之外的节点可以有直接到superstate里节点的转换。反过来,superstate里的节点可以有直接到superstate以外节点或者自身节点的转换。Superstate也可以引用自身。

9.6.2. Superstate 事件

Superstate有两个独特的事件:superstate-enter superstate-leave。不管令牌通过那个转换进入superstate的节点或者离开superstate节点,这些事件都将被激发。如果令牌执行的是superstate里的转换,这些事件将不被激发。

值的注意的是我们为statesuperstate创建了单独的事件类型。这样区分superstate事件和在superstate里传播的节点事件会变得容易。

9.6.3. 层次名称

节点名称在其范围内必须唯一。节点的范围就是节点集合。流程定义和superstate都是节点集合。为了引用superstate里的节点,你需要指定被/分割的相对名称,/用来分割节点名称。使用'..'指向上一级。下面的例子展示如何引用superstate里的节点:

<process-definition>
  ...
  <state name="preparation">
    <transition to="phase one/invite murphy"/>
  </state>
  <super-state name="phase one">
    <state name="invite murphy"/>
  </super-state>
  ...
</process-definition>

下面的例子将展示如何指向superstate的上一层次。

<process-definition>
  ...
  <super-state name="phase one">
    <state name="preparation">
      <transition to="../phase two/invite murphy"/>
    </state>
  </super-state>
  <super-state name="phase two">
    <state name="invite murphy"/>
  </super-state>
  ...
</process-definition>

9.7. 异常处理

jBPM的异常处理机制仅适用于java异常。图形执行本身不会导致错误,只有在执行委托类时才可能导致异常。

在流程定义、节点和转换上,都可以设置exception-handler的列表。每个exception-handler都有一个动作列表。当在委托类里发生异常时,会在上一层次流程元素里寻找合适的exception-handler。如果找到,则exception-handler的动作将被执行。

值的注意的是jBPM的异常处理机制与java的不完全类似。在java中,捕获的异常可以对控制流产生影响;而在jBPM中,异常处理机制不能够修改控制流。未捕获的异常将被抛给客户(例如:调用token.signal()的客户),或者被jBPM exception-handler捕获。对于捕获异常的情况,图形将继续执行,就像没有发生异常一样。

值得注意的是在处理异常的动作里,可能会通过Token.setNode(Node node)将令牌放在图中任意的节点里。

9.8. 流程组合

jBPM通过流程提供对流程组合的支持。流程状态是与另一个流程定义关联的状态。当图形执行进入流程状态时,子流程的一个新流程实例被创建,并与进入流程状态的执行路径关联。Super流程的执行路径将会等待直到子流程实例结束。当子流程实例结束时,super流程的执行路径将离开流程状态,并继续super流程图的执行。

<process-definition name="hire">
  <start-state>
    <transition to="initial interview" />
  </start-state>
  <process-state name="initial interview">
    <sub-process name="interview" />
    <variable name="a" access="read,write" mapped-name="aa" />
    <variable name="b" access="read" mapped-name="bb" />
    <transition to="..." />
  </process-state>
  ...
</process-definition>

 ‘hire’流程包含启动‘interview’子流程的流程状态。当执行进入‘first review’,将会创建一个‘interview’流程最新版本的执行(=流程实例)。‘hire’流程中的变量‘a’被拷贝到‘interview’流程的变量‘aa’。同样,‘hire’流程中的变量‘b’被拷贝到‘interview’流程的变量‘bb’。当‘interview’流程结束时,‘interview’的流程变量只有‘aa’被拷贝到‘hire’流程的变量‘a’。

通常在子流程开始之后和接到信号离开start state之前,super流程中所有读许可的变量将被读取并被填充到新创建的子流程中。当子流程实例结束时,所有写许可的变量将会从子流程拷贝到super流程。Variable元素的mapped-name属性允许你设置用于子流程的变量名称。

9.9. 定制节点行为

jBPM中,编写定制的节点类型相当容易。为了创建定制的节点,需要编写ActionHandler的实现。该实现可以执行任何业务逻辑,但是还有传递图形执行的职责。让我们看一个更新ERP系统的例子。我们将从ERP系统读取一个数量,然后把它与存储在流程变量中的数量相加,并把结果保存回ERP系统。我们需要根据数量的大小选择通过'small amounts' 还是 'large amounts'离开节点。

The update erp example process snippet

Figure 9.3. The update erp example process snippet

public class AmountUpdate implements ActionHandler {
  public void execute(ExecutionContext ctx) throws Exception {
    // business logic
    Float erpAmount = ...get amount from erp-system...;
    Float processAmount = (Float) ctx.getContextInstance().getVariable("amount");
    float result = erpAmount.floatValue() + processAmount.floatValue();
    ...update erp-system with the result...;
    
    // graph execution propagation
    if (result > 5000) {
      ctx.leaveNode(ctx, "big amounts");
    } else {
      ctx.leaveNode(ctx, "small amounts");
    }
  }
}

在定制节点的实现中,也可以创建和联合令牌。关于这方面的例子,可以查看jbpm源代码中Fork Join节点的实现

9.10. 流程图的执行

Jbpm流程图执行模型建立在流程定义和命令链模式的基础上。

流程定义的解释说明流程定义数据保存在数据库中。在流程执行过程中,需要用到流程定义信息。值得注意的是:我们使用了hibernate的二级缓存来避免在运行时加载流程定义信息。因为流程信息不会变化(参看流程版本化),所以可以使用hibernate将流程定义缓存在内存中。

命令链模式说明图中的每个节点都负责传递流程执行。如果一个节点不传递执行,那么它就是一个等待状态。

图形执行的思想就是流程实例开始执行之后,在进入等待状态之前会一直执行。

令牌代表了一条执行路线。令牌有指向流程图中节点的指针。在等待状态中,令牌可以被持久化到数据库。现在我们将要看到计算令牌执行的算法。当信号发送到令牌,执行开始。

The graph execution related methods

Figure 9.4. The graph execution related methods

当令牌停留在节点上时,可以接收信号。信号是开始执行的指令,因此必须在signal方法设置令牌所在节点的离开转换。缺省情况使用第一个转换。令牌接收到信号后,执行当前节点的业务逻辑,并调用Node.leave(ExecutionContext,Transition)方法。可以把ExecutionContext看作令牌,因为ExecutionContext里主要的对象就是令牌。Node.leave(ExecutionContext,Transition)方法将激活node-leave事件,调用Node.execute(ExecutionContext)。该方法将激活转换事件,并调用转换目的节点的Node.enter(ExecutionContext)方法。每种节点类型都在自己的execute方法中实现自己的行为。每个节点都负责通过再次调用Node.leave(ExecutionContext,Transition)传递图形的执行。概括起来:

·         Token.signal(Transition)

·         --> Node.leave(ExecutionContext,Transition)

·         --> Transition.take(ExecutionContext)

·         --> Node.enter(ExecutionContext)

·         --> Node.execute(ExecutionContext)

注意,计算下一状态的完整过程包括在客户线程中调用动作。常见的误解是所有的计算必须在客户的线程中完成,但是也可以使用异步消息(JMS)通过异步调用完成。如果发送消息和流程实例更新在同一个事务里,需要注意所有的同步问题。一些工作流系统在流程图的所有节点之间使用异步消息。但是在高吞吐的环境中,这种算法能够为消除业务流程的性能提供更多的控制和弹性。

9.11. 事务边界

Section 9.10, “Graph execution” Chapter 4, Graph Oriented Programming已经介绍,jBPM在客户的线程里运行流程本质上是同步的。也就是说,只有当流程进入一个新的等待状态token.signal() 或者 taskInstance.end()才会返回。

从建模的角度来说,在这里描述的jPDL特性是Section 4.3.4, “Asynchronous continuations”

在大多数情况下这是最直接的方法,因为流程执行可以非常方便地绑定到服务端的事务上:在一个事务里流程完成从一个状态转移到下一状态。

在一些场景中流程内部的计算需要很长时间,这种处理可能难以接受。为了处理这种情况,Jbpm提供了允许以异步方式继续执行流程的异步消息系统。当然在java企业环境中,可以配置jBPM使用JMS消息代理而不是内置的消息系统。

jPDL的所有节点都支持属性async="true"。异步节点的动作不在客户的线程中执行。相反,通过异步消息系统发送消息后线程将返回到客户(也就是说token.signal() taskInstance.end()将返回)

注意现在jbpm客户端可以提交事务。发送消息应该与流程更新在同一个事务中完成。因此提交事务的结果就是令牌已经移动到下一个节点(他还没有被执行),并且org.jbpm.command.ExecuteNodeCommand-消息已经通过异步消息系统被发送到jBPM Command Executor

jBPM Command Executor从消息队列中读取命令并执行。如果是org.jbpm.command.ExecuteNodeCommand,流程将通过执行节点继续前进。每个命令都在单独的事务中执行。

因此为了异步流程的继续执行,需要运行jBPM Command Executor。最简单的方法就是在web应用中配置CommandExecutionServlet。你可以使用其它方法,但是必须确保CommandExecutor线程已经运行起来。

作为流程建模人员,你不必真正地关注异步消息。需要记住的主要观点是事务demarcation:缺省情况下,Jbpm将在客户的事务中运行,进行整个计算直到流程进入等待状态。使用async="true"将流程中的事务demarcate

让我们看一个例子:

...
<start-state>
  <transition to="one" />
</start-state>
<node async="true" name="one">
  <action class="com...MyAutomaticAction" />
  <transition to="two" />
</node>
<node async="true" name="two">
  <action class="com...MyAutomaticAction" />
  <transition to="three" />
</node>
<node async="true" name="three">
  <action class="com...MyAutomaticAction" />
  <transition to="end" />
</node>
<end-state name="end" />
...

与流程执行交互(启动和继续)的客户端代码与正常(同步)流程完全一致:

...start a transaction...
JbpmContext jbpmContext = jbpmConfiguration.createContext();
try {
  ProcessInstance processInstance = jbpmContext.newProcessInstance("my async process");
  processInstance.signal();
  jbpmContext.save(processInstance);
} finally {
  jbpmContext.close();
}

第一个事务之后,流程实例的根令牌将指向节点1ExecuteNodeCommand消息被发送给command executor

在接下来的事务中,command executor将从队列读取消息并执行节点1。动作确定是继续传递执行还是进入等待状态。如果动作确定传递执行,那么当执行进入节点2时事务结束。依此类推。

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值