Jbpm用户指南翻译:第9章 流程建模

 
9.1 综述
流程定义(process definition)基于有向图表示了一个业务流程的规格化描述。图是由节点(node)和转换(transition)组成的,图中每个节点都有一个特定类型,节点的类型定义了运行时的行为。一个流程定义只能有一个开始状态。
令牌(token)是一个执行路线。令牌是运行时概念,它维护了一个指向图中节点的指针。
流程实例是(process instance)流程定义的执行。当一个流程实例创建后,一个令牌也为执行的主要路线创建了,这个令牌被称为流程实例的根令牌(root token),它被定位于流程定义的开始状态。
信号(signal)指示令牌继续图的执行。当令牌接收到一个没有命名的信号,它会经由缺省的离开转换离开它的当前节点;当一个转换名称在信号中被指定时,令牌会经由指定的转换离开节点。发送到流程实例的信号被委托给根令牌。
令牌进入节点后,节点被执行。节点自己有责任让图继续执行,图的继续执行是通过让令牌离开节点完成的。每个节点类型为了图的继续执行可以实现不同的行为,如果一个节点不能传播图的执行,则被表现为一个状态。
动作(Action)是在流程执行中的事件上被执行的java代码片断。在软件需求中,图是信息交流的一个重要手段,但是图只是将要生产的软件的一个视图(影像),它隐藏了很多技术细节。动作是在图的表示之外添加技术细节的一种机制,一旦图被做好,它可以由动作来修饰。主要的事件类型有:进入节点、离开节点、执行转换。
9.2 流程图
基本的流程定义是一个由节点和转换组成的图,这些信息在processdefinition.xml中表示。每个节点都有一个类型,如state、decision、fork、join等;每个节点有一组离开转换,可以给离开节点的每个转换一个名称来区分它们。例如:下图显示了jBay拍卖流程的流程图。
图 9. 1拍卖流程图
下面是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 节点
流程图是由节点和转换组成的,有关图的以及它的扩展模型的更多信息,请参考“第4章 面向图的编程”TODO。
每个节点有一个特定类型,节点类型决定了在运行时执行到达节点时将发生什么。Jbpm有一组你可以使用的预定义的节点类型,另外,你也可以编写定制代码来实现你自己指定的节点行为。
每个节点都有两个主要责任:首先,它可以执行普通java代码,典型情况下,java代码与节点功能是相关的,例如:创建一些任务实例、发送一个通知、更新一个数据库等;其次节点要负责传播流程执行。基本上来说,每个节点在传播流程执行时有以下几个可选方式:
1. 不传播执行。这种情况下节点表现为一个等待状态。
2. 经由节点的某个离开转换传播执行。这意味着到达本节点的令牌使用API调用executionContext.leaveNode()经由某个离开转换被传递,这时节点作为一个自动节点,它可以执行一些定制的程序逻辑然后自动继续流程执行,而没有等待。
3. 创建一个新的执行路径。节点可以决定创建新的令牌,每个新的令牌表示一个新的执行路径,并且每个令牌可以通过节点的离开转换被启动。这种行为的一个很关好的例子就是fork节点。
4. 结束执行路径。节点可以决定结束执行路径,这意味着令牌被结束,执行路径也就完结了。
5. 更一般的情况,节点可以修改流程实例的整个运行时结构。运行时结构是包含一个令牌树的流程实例,每个令牌表示一个执行路径,节点可以创建和结束令牌,把每个令牌放在图中的节点,并且通过转换启动令牌。
 
Jbpm像其他任何工作流和BPM引擎一样,包含一组预实现的节点类型,它们有特定的文档配置和行为,但是关于Jbpm和面向图的编程基础(第4章 面向图的编程TODO)相对于其他来说其独特之处是对开发者开放模型,开发者可以很容易的编写自己的节点行为,并在流程中使用。
这也就是传统的工作流和BPM系统非常封闭之处,它们通常提供一组固定的节点类型(叫做流程语言),它们的流程语言是封闭的并且执行模型被隐藏在运行环境中。研究 工作流模式可以发现,任何流程语言都不足够强大,我们决定做一个简单模型,并且允许开发者编写他们自己的节点类型,而JPDL流程语言则是完全开放的。
接下来我们论述JPDL中非常重要的节点类型。
9.3.2 节点类型task-node
任务节点代表一个或多个被人所执行的任务。因此当执行到达一个任务节点时,任务实例将会在工作流参与者的任务列表中被创建,然后,节点将表现为一个等待状态,当用户执行了他们的任务时,任务的完成会触发恢复执行,换句话说,这将导致一个新的信号在令牌上被调用。
9.3.3 节点类型state(状态)
状态就是一个等待状态,与任务节点不同的是没有任务实例在任务列表中被创建,如果流程需要等待一个外部系统,这是有用的。例如,在节点的入口处(通过node-enter时间的动作),可以发送一个消息到外部系统,然后流程进入等待状态,当外部系统发送一个响应消息时,这可以导致一个token.signal(),用来触发恢复流程执行。
9.3.4 节点类型decision(决策)
实际上有两种方式来建模决策,这两种方式之间的不同是基于“谁”来做决策,可以由流程(请阅读:在流程定义中指定)来做决策,或者由外部实体来提供决策结果。
当由流程来做决策时,就应该使用决策节点了。有两个基本的方法来指定决策标准:简单的方式是在转换上添加条件元素(condition),条件是返回一个布尔值的beanshell脚本表达式,在运行时,决策节点会循环它的离开转换(按照xml中指定的顺序)并计算每个条件,第一个条件结果返回为“true”的转换将被使用。另外一种可选方式是指定一个DecisionHandler的实现,然后决策在java类中被计算并且通过DecisionHandler实现的decide方法返回所选的离开转换。
当决策是由外部部分(意思是:不是流程定义的一部分)来决定时,你可以使用多个转换离开一个状态或等待状态节点,然后,离开转换可以被提供到外部,在等待状态结束后触发恢复执行,例如Token.signal(String transitionName)和TaskInstance.end(String transitionName)。
9.3.5 节点类型fork(分支)
分支把一个执行路径分裂成多个并发的执行路径。默认的分支行为是为每个离开分支的转换创建一个子令牌,并且与到达分支的令牌之间创建一个父子关系。
9.3.6 节点类型join(联合)
默认的联合假定到达联合的所有令牌都是同一父令牌的子令牌,这种情形当使用上面所提到的分支节点、并且当所有由分支创建的令牌到达同一联合时发生。联合会结束进入联合的每个令牌,然后联合会检查进入联合的令牌的父子关系,当所有的兄弟令牌都到达联合时,父令牌将会通过离开转换传播,当仍然还有活动的兄弟令牌时,联合表现为一个等待状态。
node节点类型适用于当你想要在节点中编写自己的代码的情形。node类型节点需要一个子元素动作(action),当执行到达节点时动作被执行,你写在actionhandler中的代码可以做任何事情,也包括节点必须自己负责传播执行(参考9.3.1  节点责任)TODO。
如果你想要使用JavaAPI实现一些对业务分析者来说非常重要的功能逻辑,则可以使用node类型节点,通过使用node节点,节点在流程的图形表示中是可见的。作为对比,如果逻辑对于业务分析者来说是不重要的,则动作(下面将会介绍)允许你在流程的图形表示中添加不可见的代码。
9.4 转换(Transitions)
转换有一个源节点和一个目标节点,源节点用from属性表示,目标节点用to属性表示。
节点可以有一个任意的名称,注意:很多Jbpm特性依赖于唯一的转换名称,如果有多个转换有相同的名字,则拥有给定名称的第一个转换被使用。如果在一个节点中存在重复的转换名称,则Map getLeavingTransitionsMap()方法返回的元素将少于List getLeavingTransitions()方法。
默认的转换是列表中的第一个转换。
9.5 动作(Actions)
动作是在流程执行的事件上被执行的java代码片断。在软件需求交流中图是一种重要的工具,但是图仅仅是要生产的软件的一个视图(影射),它隐藏了很多技术细节,动作是一种在图形表示之外添加技术细节的机制。一旦图被做好,它就可以用动作来进行装饰,这意味着在不改变图的结构的情况下,java代码可以与图关联。主要的事件类型是进入节点、离开节点、执行转换。
注意动作被放置在事件与被放置在节点之间的差异。放置在事件中的动作当事件激活时被执行,事件上的动作不能影响流程的控制流,这很像观察者模式。另一方面,放置在node(参考 9.3.7 节点类型node )TODO上的动作则有传播流程执行的职责(参考 9.3.1  节点责任)TODO。
让我们来看一个事件动作的例子。假设我们要在给点转换上做数据库更新,数据库更新是技术上必须的,但是对于业务分析者来说它是不重要的。
图 9. 2 数据库更新动作
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 动作配置
有关在processdefinition.xml中怎样对你定制的动作添加配置以及如何指定配置的更多信息,请参考“16.2.3配置委托”TODO。
9.5.2 动作引用
动作可以指定名称,命名的动作可以在其他需要指定动作的地方来引用,命名的动作可以作为子元素被放入流程定义中。
这个特性在你想限制动作重复配置时非常有用(例如,当动作的配置复杂时)。另外一个用处就是执行或调度运行时动作。
9.5.3 事件
事件指定了流程执行中的时刻。Jbpm引擎在图执行期间会激活事件,这发生在Jbpm计算下一个状态时(请参阅:处理信号)。事件总是同流程定义中的元素相关,例如流程定义中的一个节点或转换。大多流程元素可以激活不同类型的事件,例如节点可以激活一个node-enter事件和一个node-leave事件。事件是同动作挂钩的,每个事件有一个动作清单,当Jbpm引擎激活事件时,清单中的动作被执行。
9.5.4 事件传播
超状态在流程定义的元素之间生成一个父-子关系,节点和转换被包含在作为父的超状态里,最顶级的元素以流程定义作为父,流程定义没有父。当一个事件被激活,事件将被向上传播至父层次,这允许在一个中心位置可以捕获到流程中的所有事件以及与事件相关联的动作。
9.5.5 脚本
脚本是动作执行的beanshell脚本,关于beanshell的更多信息,请参考 beanshell站点。默认情况下,所有的流程变量可作为脚本变量和非脚本变量被写到流程变量中使用。以下脚本变量也可被使用:
l       executionContext
l       token
l       node
l       task
l       taskInstance
<process-definition>
 <event type="node-enter">
    <script>
      System.out.println("this script is entering node "+node);
    </script>
 </event>
 ...
</process-definition>
variable元素可以作为脚本的子元素,用来定制默认的加载和存储变量到脚本的行为。如果是那样的话,脚本表达式还必须被放入脚本的子元素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>
在脚本开始之前,流程变量YYY和ZZZ分别作为脚本变量b和c将会被脚本所使用,脚本结束之后,脚本变量a的值被存储到流程变量XXX中。
如果变量的access属性包含“read”,则流程变量在脚本计算前作为脚本变量被加载;如果access属性包含“write”,则在脚本计算后脚本变量将被作为流程变量存储。属性mapped-name可以使流程变量在脚本中以另外一个名字使用,当你的流程变量包含空格或其他非法脚本字符时这很有用。
9.5.6 定制事件
注意,在流程执行期间激活你自己的定制事件是可能的。事件通过组合图元素(节点、转换、流程定义、超状态是图元素)和事件类型(java.lang.String)被唯一的定义。Jbpm定义了一组可由节点、转换和其他图元素激活的事件,但是作为一个用户,你可以自由的激活你自己的事件,在动作中、在你自己定制的节点执行中、或者甚至在流程实例执行之外,你都可以调用GraphElement.fireEvent(String eventType, ExecutionContext executionContext),事件类型的名称可以自由选择。
9.6 超状态(Superstates)
超状态是一组节点,超状态可以被递归嵌套。超状态可以被用来在流程定义中产生一些层次,例如,一个应用可能要把流程中的所有节点按阶段进行分组。动作可以与超状态事件关联,结果就是一个令牌在某个给定时间可以存在于多个嵌套的节点,这便于检查流程是否执行,比如,是否在启动阶段。在Jbpm模型中,你可以任意分组任何节点到一个超状态。
9.6.1 超状态转换
所有离开超状态的转换都可以被包含在超状态的节点里的令牌使用,转换也可以到达超状态,如果那样的话,令牌将被重定向到超状态中的第一个节点。超状态外部的节点可以拥有指向超状态内部的转换,同样,相反的,超状态内部的节点也可以拥有指向超状态外部或超状态自己的转换。超状态还可以拥有对它自己的引用。
9.6.2 超状态事件
有两个只有超状态才有的事件:superstate-enter和superstate-leave。无论通过哪个转换进入或离开节点,这些事件都会被激活。在超状态内部令牌执行转换时,这些事件不会被激活。
9.6.3 分级命名
节点在它的范围之内必须被唯一命名,节点的范围是它自己的节点集合,流程定义和超状态都是节点集合。在指向超状态内部节点时,你必须指定相对关系,用(/)隔开节点名称,用“..”指向上一层次。下面的例子展示了怎样指向一个超状态内部的节点:
<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>
下面的例子展示了怎样向上指向超状态层次:
<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异常,图本身的执行不会导致问题,只有在执行委托类时才会导致异常。
在流程定义(process-definitions)、节点(nodes)和转换(transitions)上,可以指定一个异常处理(exception-handlers)清单,每个异常处理(exception-handler)有一个动作列表,当在委托类中发生异常时,会在流程元素的父层次搜索一个适当的异常处理(exception-handler),当它被搜索到,则异常处理(exception-handler)的动作将被执行。
注意,Jbpm的异常处理机制与java异常处理不完全相似。在java中,一个捕获的异常可以影响控制流,而在Jbpm中,控制流不会被Jbpm异常处理机制所改变。异常要么被捕获,要么不捕获,没有被捕获的异常被抛向客户端(例如客户端调用token.signal()),而被捕获的异常则是通过Jbpm的exception-handler,对于被捕获的异常,图执行仍会继续,就像没有异常发生一样。
注意,在处理异常的动作中,可以使用Token.setNode(Node node)把令牌放入图中的任何节点。
9.8 流程组合
在Jbpm中依靠process-state来支持流程的组合。process-state是与另外一个流程定义关联的状态,当图执行到达process-state,一个新的子流程实例被创建,并且它与到达process-state的执行路径相关联,超流程的执行路径将会等待,直到子流程实例结束。当子流程实例结束时,超流程的执行路径将离开process-state并在超流程中继续图的执行。
<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”流程的process-state,当执行到达“initial interview”,最新版本的“interview”流程的一个新的执行(相当于流程实例)被创建,然后来自“hire”流程的变量“a”被拷贝到来自“interview”流程的变量“aa”,同样,“hire”流程的变量“b”被拷贝到“interview”流程的变量“bb”。当“interview”流程结束时,只有“interview”流程的变量“aa”被拷贝回“hire”流程的变量“a”。
通常,当一个子流程被启动,在离开开始状态的信号被发出之前,所有拥有“read”存取属性的变量都被从超流程载入新创建的子流程;当子流程结束时,所有拥有“write”存取属性的变量都被从子流程拷贝到超流程。variabled元素的mapped-name属性允许你指定在子流程中将使用的变量名称。
9.9 定制节点行为
在Jbpm中很容易编写你自己的定制节点。为了创建定制节点,ActionHandler的一个实现必须被编写,这个实现可以执行任何业务逻辑,也必须包括传播图执行的职责。让我们看一个更新ERP系统的例子:我们将会从ERP系统中读取一个数量,然后加上一个存储在流程变量中的数量,再把结果存储回ERP系统;基于数量的大小,我们必须通过“small amounts”或“big amounts”转换离开节点。
图 9. 3 更新erp流程示例片断
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");
    }
 }
}
在定制的节点实现中可以创建和联合(join)令牌,关于如何做的示例,请签出Jbpm源码中fork和join节点的实现部分:-)
9.10 图执行
Jbpm的图执行模型基于的是流程定义的解释和命令链设计模式(chain of command pattern)。
解释流程定义意思是把流程定义存储到数据库中,在运行时流程执行期间流程定义信息被使用。需要注意的是:我们使用hibernate的二级缓存来避免在运行时加载流程定义信息,因为流程定义不会改变(参考流程版本部分),hibernate可以在内存中缓存流程定义。
命令链设计模式意味着图中每个节点负责传播流程执行,如果一个节点不传播执行,则它表现为一个等待状态。
这个意思是说流程实例开始执行之后一直执行,直到进入一个等待状态。
令牌代表一个执行路径,令牌拥有一个指向流程图中节点的指针,在等待状态,令牌可以被持久化到数据库。现在让我们看看计算令牌执行的算法:当一个信号发送到令牌时执行开始,然后执行通过转换和节点使用命令链设计模式被传递。下面是类图中的相关方法:
图 9. 4 图执行相关方法
当令牌在节点内时,信号可以被发送到令牌,发送信号是开始执行的指示。信号必须指定令牌当前节点的离开转换,默认的是第一个离开转换。信号发送到令牌,令牌获取它的当前节点并且调用Node.leave(ExecutionContext, Transition)方法,可以把ExecutionContext看作令牌,因为ExecutionContext中的主要对象就是一个令牌。Node.leave(ExecutionContext, Transition)方法会激活转换事件并且调用转换的目标节点的Node.enter(ExecutionContext)方法,而Node.enter(ExecutionContext)方法会激活node-enter事件并且调用Node.execute(ExecutionContext)方法。每种类型的节点在execute方法中都有它们自己实现的行为,每个节点有责任通过再调用Node.leave(ExecutionContext, Transition)传播图的执行。概括如下:
l       Token.signal(Transition)
l       -->Node.leave(ExecutionContext, Transition)
l       -->Transition.take(ExecutionContext)
l       -->Node.enter(ExecutionContext)
l       -->Node.execute(ExecutionContext)
注意,完成下一个状态的计算,包括调用动作是在客户端线程中完成的。一个误解是所有计算必须在客户端线程完成,象任何异步调用,你也可以使用异步消息(JMS)来完成。当消息被发送到与流程实例更新的同一事务中时,所有的同步问题必须小心对待。某些工作流系统在图的所有节点之间使用异步消息,除了在高吞吐量的环境里之外,这个算法可以给业务流程性能调整提供更多的控制和灵活性。
9.11 事务划分
象“9.10图执行”TODO和“第4章 面向图的编程”TODO中解释的那样,Jbpm在客户端线程中运行流程,并且自然使用同步。这个意思是说,token.signal()或者taskInstance.end()只有当流程进入一个新的等待状态时才会返回。
这里我们描述的JPDL特性来自于“第13章 异步继续”的建模观点。
因为流程执行可以很容易同事务服务绑定在一起,所以在很多情况下这是一个非常直观的方法:在一个事务中流程从一个状态到另一个状态。
在某些情形,流程中的某个计算会花费很长时间,这个行为是不受欢迎的,Jbpm包含一个允许以一种异步方式来继续流程的异步消息系统用来应对这种情况。当然,在java企业应用环境,Jbpm可以被配置为使用JMS消息代理,用来代替自己所构造的消息系统。
在某些节点,JPDL支持属性async=“true”,异步节点不会在客户端线程被执行,而是通过异步消息系统发送一个消息,并且线程被返回给客户端(意味着是token.signal()或taskInstance.end()返回)。
注意,现在Jbpm客户端代码可以提交事务。消息的发送应该与流程的更新在同一事务中完成。因此事务的最后结果是令牌被移动到下一节点(尚未被执行),并且一个org.jbpm.command.ExecuteNodeCommand消息在异步消息系统上被发送到Jbpm的命令执行器(Command Executor)。
Jbpm的命令执行器从队列中读取并执行命令,在org.jbpm.command.ExecuteNodeCommand下,流程通过执行节点被继续,每个命令在一个独立的事务中被执行。
因此,为了异步流程可以继续,必须需要运行一个Jbpm命令执行器,一个简单的方法是在你的Web应用中配置CommandExecutionServlet,作为选择,你应确保命令执行器线程可以运行于任何其他方式。
作为一个流程建模者,你不应该被这些异步消息所干预,主要关注点是事务划分:默认情况下Jbpm在客户端事务中运转,进行全部计算,直到进入一个等待状态。使用async=“true”在流程中划分事务。
让我们看一个例子:
...
<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();
}
在第一个处理之后,流程实例的根令牌将指向节点one,并且一个ExecuteNodeCommand消息被发送到命令执行器。
在并发的事务中,命令执行器从队列中读取消息,并且执行一个节点,动作可以决定传播执行或进入一个等待状态。如果动作决定传播执行,则当执行到达节点 two 时事务会被结束,以此类推  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值