jbpm5.1介绍(1)

快速开始 

首先下载jBPM,http://sourceforge.net/projects/jbpm/files/

可以有选择性的下载:

  • bin:jBPM的二进制文件和它们的依赖包
  • src:核心模块的源码
  • gwt-console:jBPM的控制台,包括服务端和客户端
  • docs:文档
  • examples:一些jBPM的例子,可以导入到Eclipse
  • installer: jBPM的安装,下载和安装一个jBPM的示例
  • installer-full:所有的包括demo,jar包等待的完整工程

一些有用的链接 

源码 

jBPM的,现在使用它的源代码版本控制系统的git。可以在这里找到jBPM项目的来源(包括所有版本从jBPM5.0- CR1开始):

https://github.com/droolsjbpm/jbpm

安装所需条件

 JDK 1.5+ (set as JAVA_HOME)

 Ant 1.7+

演示环境安装

到安装目录下运行

ant install.demo

将会执行

  • 下载JBoss AS
  • 下载Eclipse
  • JBoss的安装Drools的Guvnor
  • 到JBoss Oryx安装设置
  • 安装到JBoss jBPM的控制台
  • 安装jBPM的Eclipse插件
  • 安装Drools的Eclipse插件

如果你想看到报告在jBPM控制台上,那么需要修改build.properties文件的jBPM.birt.download属性设置为true

ant start.demo

启动示例

  • 启动H2数据库
  • 启动了JBoss AS
  • 启动Eclipse
  • 启动人工任务服务

使用Eclipse tools

导入示例工程下的sample/evaluation

导入之后可以看到工程中的示例程序

双击打开Evaluation.bpmn

可以运行ProcessTest进行测试

使用jBPM控制台

启动后输入如下链接

http://localhost:8080/jbpm-console

使用 krisv / krisv 登录

可以看到如下界面

你可以启动一个新的流程,查看一个正在运行的流程的实例的状态,查看你的任务,完成任务,监控流程的执行

使用Guvnor仓库和设计

作为一个过程库的Guvnor库可用于存储业务流程。它还提供了一个基于Web的界面来管理您的进程。

输入如下地址可以进入

http://localhost:8080/drools-guvnor

核心引擎API

本节介绍的API,你需要加载过程并执行它们。对于如何界定的过程本身更详细,查看检出的BPMN 2.0的章节。

你可以在知识库中定义一个流程实例,然后在知识库中产生一个实例的session对象,如下图所示

知识库可以共享会话之间,通常只创建一次,在启动应用程序。知识库可以动态改变(这样你就可以在运行过程中添加或删除)。

会话可以创建基于一个知识库,用于执行过程,并与引擎交互。你想创建一个会话被认为是相对较轻的,你可以创造尽可能多的独立会议。如何创建许多会议是由你。在一般情况下,最简单的情况下开始创建一个会话,然后在您的应用程序的各个地方。你可以决定创建多个会话,例如,如果你想有多个独立的处理单元(例如,你想要的所有进程,从一个客户的完全独立于另一个客户的过程,使您可以创建一个为每个客户独立会议),或如果你需要多个会话,可扩展性的原因。如果你不知道做什么,只要简单地启动一个知识库,其中包含你所有的流程定义和创建会话,然后使用执行你所有的流程。

正如上文所述,jBPM的API,因此可用于:(1)创建一个知识库,其中包含您的流程定义(2)创建一个会话启动新的进程实例,信号现有注册侦听等。

1)知识库

通过知识库加载流程定义,通过以下代码实现

?
KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
kbuilder.add(ResourceFactory.newClassPathResource( "MyProcess.bpmn" ), ResourceType.BPMN2);
KnowledgeBase kbase = kbuilder.newKnowledgeBase();

 ResourceFactory有类似的方法来加载文件系统的文件,从URL,InputStream中,等

2)Session

一旦你加载你的知识库,你应该创建一个会话与引擎交互。本次会议可以被用来启动新的进程,信号事件等,下面的代码片段显示了它是多么容易创建较早创建的知识库为基础的会话,并启动一个进程(ID)。

?
StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession();
ProcessInstance processInstance = ksession.startProcess( "com.sample.MyProcess" );

 ProcessRuntime接口定义了所有的会议方法与流程交互,如下所示。

?
/**
      * Start a new process instance.  The process (definition) that should
      * be used is referenced by the given process id.
     
      * @param processId  The id of the process that should be started
      * @return the ProcessInstance that represents the instance of the process that was started
      */
     ProcessInstance startProcess(String processId);
  
     /**
      * Start a new process instance.  The process (definition) that should
      * be used is referenced by the given process id.  Parameters can be passed
      * to the process instance (as name-value pairs), and these will be set
      * as variables of the process instance. 
     
      * @param processId  the id of the process that should be started
      * @param parameters  the process variables that should be set when starting the process instance 
      * @return the ProcessInstance that represents the instance of the process that was started
      */
     ProcessInstance startProcess(String processId,
                                  Map<String, Object> parameters);
  
     /**
      * Signals the engine that an event has occurred. The type parameter defines
      * which type of event and the event parameter can contain additional information
      * related to the event.  All process instances that are listening to this type
      * of (external) event will be notified.  For performance reasons, this type of event
      * signaling should only be used if one process instance should be able to notify
      * other process instances. For internal event within one process instance, use the
      * signalEvent method that also include the processInstanceId of the process instance
      * in question. 
     
      * @param type the type of event
      * @param event the data associated with this event
      */
     void signalEvent(String type,
                      Object event );
  
     /**
      * Signals the process instance that an event has occurred. The type parameter defines
      * which type of event and the event parameter can contain additional information
      * related to the event.  All node instances inside the given process instance that
      * are listening to this type of (internal) event will be notified.  Note that the event
      * will only be processed inside the given process instance.  All other process instances
      * waiting for this type of event will not be notified.
     
      * @param type the type of event
      * @param event the data associated with this event
      * @param processInstanceId the id of the process instance that should be signaled
      */
     void signalEvent(String type,
                      Object event ,
                      long processInstanceId);
  
     /**
      * Returns a collection of currently active process instances.  Note that only process
      * instances that are currently loaded and active inside the engine will be returned.
      * When using persistence, it is likely not all running process instances will be loaded
      * as their state will be stored persistently.  It is recommended not to use this
      * method to collect information about the state of your process instances but to use
      * a history log for that purpose.
     
      * @return a collection of process instances currently active in the session
      */
     Collection<ProcessInstance> getProcessInstances();
  
     /**
      * Returns the process instance with the given id.  Note that only active process instances
      * will be returned.  If a process instance has been completed already, this method will return
      * null.
     
      * @param id the id of the process instance
      * @return the process instance with the given id or null if it cannot be found
      */
     ProcessInstance getProcessInstance( long processInstanceId);
  
     /**
      * Aborts the process instance with the given id.  If the process instance has been completed
      * (or aborted), or the process instance cannot be found, this method will throw an
      * IllegalArgumentException.
     
      * @param id the id of the process instance
      */
     void abortProcessInstance( long processInstanceId);
  
     /**
      * Returns the WorkItemManager related to this session.  This can be used to
      * register new WorkItemHandlers or to complete (or abort) WorkItems.
     
      * @return the WorkItemManager related to this session
      */
     WorkItemManager getWorkItemManager();

3)Events

可以使用ProcessEventListener注册自己的监听器

?
public interface ProcessEventListener {
  
   void beforeProcessStarted( ProcessStartedEvent event );
   void afterProcessStarted( ProcessStartedEvent event );
   void beforeProcessCompleted( ProcessCompletedEvent event );
   void afterProcessCompleted( ProcessCompletedEvent event );
   void beforeNodeTriggered( ProcessNodeTriggeredEvent event );
   void afterNodeTriggered( ProcessNodeTriggeredEvent event );
   void beforeNodeLeft( ProcessNodeLeftEvent event );
   void afterNodeLeft( ProcessNodeLeftEvent event );
   void beforeVariableChanged(ProcessVariableChangedEvent event );
   void afterVariableChanged(ProcessVariableChangedEvent event );
  
}

 默认支持下面的记录器实现:

1。 控制台记录器:此记录写入控制台的所有事件。
2。 文件记录器:此记录写入到一个文件中使用XML表示的所有事件。此日志文件可能被用来在IDE中生成一个基于树的可视化,在执行过程中发生的事件。
3。 线程文件记录器:因为文件记录器的事件写入到磁盘中,只有当关闭记录仪或记录器中的事件数量达到预定水平时,它不能被用来调试时,在运行过程。一个线程文件记录器的事件写入到一个文件后,在指定的时间间隔,使得它可以使用记录器以可视化的实时进展,而调试过程。

KnowledgeRuntimeLoggerFactory 可以添加logger到你的session中

?
KnowledgeRuntimeLogger logger =
     KnowledgeRuntimeLoggerFactory.newFileLogger( ksession, "test" );
// add invocations to the process engine here,
// e.g. ksession.startProcess(processId);
...
logger.close();

业务流程创建

使用eclipse创建流程

可以在生成好的文件上定义业务流程

节点类型描述

BPMN 2.0规范定义了三种主要类型的节点

事件:它们用于模型中的特定事件的发生。这可能是一个开始事件(即用来指示的过程中开始),结束事件(定义过程结束,或该子流)和中间事件(指示的执行过程中可能出现的事件过程)。

活动:这些定义,需要在执行过程中执行不同的动作。存在不同类型的任务,对活动的类型取决于您尝试模型(如人工的任务,服务任务等)和actvities也可以嵌套(使用不同类型的子进程)。

网关:可以被用来定义多个路径的过程中。根据网关类型,这些可能表明并行执行,选择等

流程属性

一个BPMN2过程是不同类型的节点与使用连接流程图。这个过程本身暴露了以下属性:

  • id:过程中的唯一的ID。
  • name:过程中的显示名称。
  • Version: 版本号的过程。
  • Package: 过程的包(命名空间)
  • Variables:变量可以被定义为数据存储过程的执行期间
  • Swimlanes:指定在这个过程中用于分配人工任务的泳道

事件

1)开始事件

进程的开始。一个过程应该有一个起始节点,没有传入的连接只有传出的连接

每当一个进程启动后,将开始执行此节点,并自动继续与这个启动事件的第一个节点,并依此类推。它包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称

2)结束事件

所有流程的结束,应该只有传入的连接没有传出的连接。它包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称

Terminate: 结束事件可以在整个过程中或终止路径。当一个流程实例被终止,这意味着它的状态设置为完成和所有其他节点可能仍然活跃在这个过程实例(并行路径)被取消。非终止结束事件只是这个路径(执行这个分支将在这里结束)结束,但仍然可以继续其他平行的路径。如果有流程实例内,没有更积极的的路径(例如,如果一个流程实例到达结束节点,但非终止流程实例内有没有更活跃的分支,将完成的过程实例,一个流程实例将自动完成反正)。终止结束事件的可视化的事件节点内使用一个完整的圆,非终止事件节点是空的。请注意,如果您使用一个子进程内的终止事件节点,你是终止流程实例的顶层,不只是子进程。

3)出错事件

错误处理事件,只能有传入事件没有传出事件,错误事件包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • FaultName: 故障名称,使用些名称来处理故障
  • FaultVariable: 名称的变量,它包含与此故障相关的数据。这个数据也是通过异常处理程序(如果找到)。

4)定时器事件

表示定时器后,可以触发一个特定的时间内一次或多次。计时器事件应该有一个传入的连接和一个外向连接。计时器延迟指定计时器之前应等待多久引发的第一次。当计时器事件的过程中达到的,它会启动相关的定时器。如果定时器节点被取消(例如,通过完成或中止封闭的过程实例),定时器就会被取消。

计时器事件包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • Timer delay:延迟,节点之前应等待触发第一次。表达应的形式[#][#H] [#M] [#S] [#[MS]。这意味着,您可以指定天,小时,分钟,秒和multiseconds(这是默认的,如果你不指定任何)的数量。例如,表达1H”触发定时器将等待一个小时。表达式也可以使用#{expr}的动态推导基于一些过程变量的延迟。 EXPR在这种情况下,可能是一个过程变量,或基于一个过程变量(如myVariable.getValue())的一个更复杂的表达式。
  • Timer period: 随后的两个触发器之间的期间。如果期间为0时,定时器应只能触发一次。表达应的形式[#][#H] [#M] [#S] [#[MS]。这意味着,您可以指定天,小时,分钟,秒和multiseconds(这是默认的,如果你不指定任何)的数量。例如,表达1H”再次触发定时器将等待一个小时。也可以使用#{expr}的动态推导基于一些过程变量期间的表达。 EXPR在这种情况下,可能是一个过程变量,或基于一个过程变量(如myVariable.getValue())的一个更复杂的表达式。

5)信号事件

可用于信号事件,在执行过程中的内部或外部事件作出回应。信号事件有没有传入的连接和一个外向连接。它指定的事件类型,预计。每当检测到这种类型的事件,此事件节点相连的节点将被触发。它包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • EventType:事件的类型
  • VariableName: variablename的变量将包含与此事件相关的数据(如有),此事件发生时的名称。

一个流程实例可以标志着一个特定的事件发生

?
ksession.signalEvent(eventType, data, processInstanceId)

这将触发所有在给定的进程实例(活动)信号,等待该事件类型的事件节点。与事件相关的数据可以通过使用数据参数。如果事件节点指定一个变量名,这个数据将被复制到该变量在事件发生时。

您还可以产生一个流程实例内的信号。可以使用一个脚本(脚本任务或进入或退出操作使用)

?
kcontext.getKnowledgeRuntime().signalEvent(
   eventType, data, kcontext.getProcessInstance().getId());

6)活动

表示,应该在这个过程中执行的脚本。脚本任务应该有一个传入的连接和一个外向连接。指定应执行相关的操作,编码的行动(即Java或MVEL)使用的方言,和实际行动代码。此代码可以访问的任何变量和全局。还有一个预定义的变量kcontext引用ProcessContext对象,例如,它可以被用来访问当前流程实例或NodeInstance,并获得和设置变量,或获得ksession使用kcontext.getKnowledgeRuntime()。当一个脚本任务的过程中达成的,它会执行的动作,然后继续下一个节点。它包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • Action: 行动节点与此相关的动作脚本

请注意,您可以编写脚本节点内的任何有效的Java代码。这基本上允许你做这样一个脚本节点内部任何。但是也有一些注意事项:

  • 当试图建立一个更高级别的业务过程中,也应该由企业用户了解,它可能是明智的,里面的过程中避免低层次的实施细节,包括在这些脚本任务。脚本任务仍然可以用于快速操作变量等,但其他概念,如服务任务,可以用来在一个更高层次的方式更复杂的行为模式。
  • 应立即脚本。他们使用的是引擎的线程来执行脚本。也许应该仿照作为异步服务任务,可能需要一些时间来执行的脚本。
  • 您应尽量避免接触外部服务,通过一个脚本节点。这不仅平时违反前两个警告,这也与外部服务交互的发动机,可问题没有的知识,特别是当使用持久性和交易。在一般情况下,它可能是更明智的使用服务的任务与外部服务模式通信。
  • 脚本不应该抛出异常。运行时异常应该被捕获和管理里面的脚本或转换成信号或,然后将其内部的过程中处理错误的例子。

7)服务任务

执行流程引擎之外的所有工作应派代表参加(以声明方式)使用服务任务。不同类型的服务是预定义的,例如,发送电子邮件,记录信息等,用户可以定义域特定的服务或工作项目,采用了独特的名称和定义的参数(输入)和相关的结果(输出)这种类型的工作。检查章特定于域的过程进行了详细的解释和例子说明如何定义和使用在你的流程的工作项目。当一个服务任务的过程中达成共识,相关的工作是执行。一个服务的任务应该有一个传入的连接和一个外向连接。包含如下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • Parameter mapping:允许过程变量的值复制到工作项目的参数。创建工作项目时,该值将被复制。
  • Result mapping: 允许复制工作项目的结果参数值的过程变量。每种类型的工作可以定义结果,将(可能)已经完成的工作项目后返回的参数。结果映射可用于复制的给定的结果参数值,在这个过程中给定的变量。例如,FileFinder”工作项目返回结果参数文件内匹配给定的搜索条件的文件清单。这个文件的列表,然后可以绑定以便在使用过程中的一个过程变量。工作项目完成后,该值将被复制。
  • On-entry and on-exit actions:在进入或退出这个节点,分别执行的动作。
  • Additional parameters: 每个工作项目类型可以定义额外的参数,这种类型的工作有关。例如,“电子邮件”工作项目定义额外的参数,如发件人,收件人,主题和正文。用户既可以提供这些参数的值,直接或定义一个参数的映射,将复制在这个过程中给定的变量给定的参数值,如果两者都指定,映射将具有优先权。 String类型的参数,可以使用#{表达式}嵌入在字符串值中。创建工作项目时,该值将被检索和替换表达式将被替换变量上调用toString()方法的结果。表达式可以简单地将一个变量的名字(在这种情况下,解析变量的值),但更先进的MVEL表达式尽可能,如#{person.name.firstname}。

8)用户任务

 

过程也涉及用户需要执行的任务。用户任务代表一个原子由一个人执行的任务。它应该有一个传入的连接和一个外向连接。可用于用户任务与泳道分配多个人工任务相似的用户组合。请参阅有关详细信息,对用户的任务章。用户任务实际上只是一个服务节点的具体类型(类型为“人工任务”)。用户任务包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • TaskName: 任务的名称
  • Priority: 一个整数,指示任务的优先级
  • Comment:与任务相关的描述
  • ActorId: 负责执行人工任务的ID。一个演员的ID列表,可以指定使用一个逗号(',')作为分隔。
  • GroupId: 负责执行人工任务的组ID。作为分隔符使用一个逗号(',')可以指定一组ID的列表。
  • Skippable: 指定是否可以跳过人工任务,即,这个角色可能会决定不执行任务。
  • Content: 与此任务相关的数据。
  • Swimlane:泳道可以分配多个人工任务给相同的角色
  • On entry and on exit actions: 在进入和退出之前分别执行的动作
  • Parameter mapping: 允许人工任务的参数复制过程变量的值。创造人工的任务后,该值将被复制。
  • Result mapping: 允许复制工作项目的结果参数值的过程变量。每种类型的工作可以定义结果,将(可能)已经完成的工作项目后返回的参数。结果映射可用于复制的给定的结果参数值,在这个过程中给定的变量。例如,FileFinder”工作项目返回结果参数文件内匹配给定的搜索条件的文件清单。这个文件的列表,然后可以绑定以便在使用过程中的一个过程变量。工作项目完成后,该值将被复制。

9)子流程

表示从这个过程中的另一个进程调用。一个子流程节点都应该有一个传入的连接和一个传出连接。当可重复使用的子流程节点的过程中达成的,该发动机将开始与给定ID的过程。它包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • ProcessId: 应执行的进程的ID
  • Wait for completion: 如果此属性为true,这个子进程的节点只会继续下去,如果启动的子进程已终止其执行(完成或中止),否则将继续启动子后,立即(所以它不会等待它的完成)。
  • Independent: 如果此属性为true,子进程开始作为一个独立的过程,这意味着如果完成这个父进程,子进程将不会被终止(或其他一些原因取消此子流程节点);活跃​​的子进程将被取消(或取消子流程节点)父进程终止。
  • On entry and on exit actions: 在进入和退出之前分别执行的动作
  • Parameter in/out mapping: 子流程节点还可以定义和映射变量。启动进程时,在“在”映射的变量将被用作参数(相关参数名称)。所定义的“出”映射的子进程的变量将被复制到这个过程中,变量,当子进程已经完成。请注意,您可以使用“出”映射只有当“等待完成”设置为true。

10)业务规则任务

表示需要进行评估的规则,。到达节点时,规则进行评估。规则的任务应该有一个传入的连接和一个外向连接。规则是被定义在单独的文件中使用Drools的规则格式。规则可以成
为一个特定的规则流使用规则流组属性组规则头的一部分。当一个规则任务达成的过程中,发动机将开始执行相应的规则流组的部分(如有)的规则。执行将自动继续到下一个
节点,如果有没有更积极的规则在这个规则流组。这意味着,在执行一个规则流组,有可能属于当前活动的规则流组的新激活添加其他规则的事实的变化,由于议程。请注意,这
一进程将立即继续下一个节点,如果遇到一个规则流组,那里有当时没有活动的规则。如果规则流组已经启动,规则流组将保持活跃,只会继续执行规则流组的所有活动的规则,
如果已经完成。它包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • RuleFlowGroup: 表示这个RuleFlowGroup节点规则集的规则流组的名称。

11)嵌入式子过程

一个子进程是一个节点,它可以包含其他节点,因此,它作为一个节点的容器行为。这使得不仅在这样一个子流程节点的嵌入过程的一部分,而且此容器内部的所有节点访问的额外的变量的定义。一个子进程应该有一个传入的连接和一个外向连接。它也应该包含一个起始节点定义启动(子进程内),当你到达子进程。它也应该包含一个或多个结束事件。需要注意的是,如果您使用里面一个子进程终止的事件节点,则终止流程实例的顶层,不只是子进程,所以一般你应该使用一个子进程内非终止端节点。当有活动的节点内的子进程没有子进程结束。它包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • Variables: 变量可以被定义为在执行此节点的数据存储。

12)多实例的子进程

多实例的子进程是一种特殊的子进程,允许你执行所包含的过程中多次分部,为每一个集合中的元素一次。多实例子进程应该有一个传入的连接和一个传出连接。等待,直到完成嵌入式的过程片段是在给定集合的每个元素,然后再继续。它包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • CollectionExpression: 一个变量,表示应该遍历元素的集合的名称。集合变量应该是一个数组或类型的java.util.Collection。如果集合表达式计算为null或空的集合,子进程将立即完成的多个实例,并按照其传出连接。
  • VariableName:variablename的变量的名称:包含从集合中的当前元素。这给选定的元素的复合节点访问的节点。

13)流向不同的网关

允许你创建你的进程中的分支。发散网关应该有一个传入的连接和两个或两个以上的传出连接。目前支持的网关节点有三个类型:

  • 并行的手段,控制流将同时继续在所有传出连接。
  • 异或完全传出连接将选择的唯一手段。这个决定是由评估链接到每个传出连接的约束。选择优先级最低的数量,计算结果为true的约束。约束可以指定使用不同的方言。请注意,您应始终确保至少一个传出连接会在运行时评估为true(在规则流在运行时会抛出一个异常,如果它不能找到至少一个传出连接)。
  • OR或,选择的条件计算结果为true的所有传出连接的手段。条件相似的独家网关,没有重点考虑的除外。请注意,因为这个过程在运行时会抛出一个异常,如果它不能确定传出连接,你应该确保至少一个传出连接将评估在运行时也是如此。

它包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • Type:分割节点,即和,异或OR(见上文)的类型
  • Constraints: 约束与每个传出连接

可以同步多个分支。融合网关应该有两个或两个以上的传入连接和一个外向连接。有两种类型的拆分目前支持:

它包含以下属性:

  • Id: 节点的ID(这是一个节点容器内唯一)
  • Name: 节点的显示名称
  • Type:分割节点,即和,异或OR(见上文)的类型

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值