说说 jBPM 流程定义语言(7)—— task(人工任务活动)

task 活动用来处理涉及人机交互的活动。它的功能在 jBPM 乃至整个工作流的应用中都具有极其重要的意义,因为处理人工任务、电子表单是工作流应用中最繁琐与细致的工作。

1 任务的分配者

利用 task 活动的 assignee 属性(分配者属性)可以把一个任务分配给指定的用户。

属性类型默认值是否必需描述
assignee表达式可选被分配到任务的用户 ID

 任务分配者的流程示例

jPDL:

<?xml version="1.0" encoding="UTF-8"?>

<process key="TaskAssignee" name="TaskAssignee" xmlns="http://jbpm.org/4.4/jpdl">
   <start g="168,181,48,48" name="start1">
      <transition to="审核"/>
   </start>
   <task assignee="#{order.owner}" g="307,179,92,52" name="审核">
      <transition to="等待"/>
   </task>
   <state g="456,182,92,52" name="等待"/>
</process>

这里演示了任务分配的两个方面:

  • assignee 属性定义了一个账号,即负责完成任务的人。
  • assignee 属性默认以 EL 表达式来执行,示例中从任务对应的流程变量中查找 order 对象内的 owner 值。

order 对象的类定义:

public class Order implements Serializable {
  
  String owner;

  public Order(String owner) {
    this.owner = owner;
  }

  public String getOwner() {
    return owner;
  }

  public void setOwner(String owner) {
    this.owner = owner;
  }
}

测试代码:

Map<String, Object> variables = new HashMap<>();
variables.put("order", new Order("deniro"));

//发起实例
ProcessInstance processInstance = executionService.startProcessInstanceByKey
		("TaskAssignee", variables);

//获取 deniro 的任务列表
List<Task> taskList = taskService.findPersonalTasks("deniro");
assertTrue(taskList.size() > 0);

注意: assignee 属性也可以是纯文本。

2 任务的候选者

也可以将任务分配给一组候选用户,组中的每一个用户可以接受这个任务并完成它,这就是任务的候选者机制。

属性类型默认值是否必需描述
candidate-groups表达式可选使用逗号分隔的用户 ID 列表。所有组中的用户将会成为任务的候选者。
candidate-users表达式可选使用逗号分隔的用户 ID 列表。所有列表中的用户将会成为任务的候选者。

 任务候选者的流程示例

jPDL:

<?xml version="1.0" encoding="UTF-8"?>

<process key="TaskCandidates" name="TaskCandidates" xmlns="http://jbpm.org/4.4/jpdl">
   <start g="168,181,48,48" name="start1">
      <transition to="审核"/>
   </start>
   <task candidate-groups="sales-dept" g="307,179,92,52" name="审核">
      <transition to="等待"/>
   </task>
   <state g="456,182,92,52" name="等待"/>
</process>

测试代码:

//创建组
identityService.createGroup("sales-dept");

//创建用户
identityService.createUser("deniro", "deniro", "li");
identityService.createUser("lily", "deniro", "lily");

//加入组
identityService.createMembership("deniro", "sales-dept");
identityService.createMembership("lily", "sales-dept");

//发起实例
ProcessInstance processInstance = executionService.startProcessInstanceByKey
		("TaskCandidates");


//review 任务出现在用户的分组任务列表中
assertFalse(taskService.findGroupTasks("deniro").isEmpty());
assertFalse(taskService.findGroupTasks("lily").isEmpty());

//deniro 接受了任务
String taskId = taskService.findGroupTasks("deniro").get(0).getId();
taskService.takeTask(taskId, "deniro");

//任务从所有候选者的任务列表中消失
assertTrue(taskService.findGroupTasks("deniro").isEmpty());
assertTrue(taskService.findGroupTasks("lily").isEmpty());

//出现在 deniro 的已分配任务列表中
assertFalse(taskService.findPersonalTasks("deniro").isEmpty());
  • 这里创建了两个账号并把他们加入了一个组(使用 IdentityService 身份认证服务)。
  • 候选者在处理任务之前,必须先接受任务。
  • 一个候选者任务如果被某个候选者接受后,它就会从所有的候选者任务列表中消失,并只出现在这个候选者的已分配任务列表中。

注意:在客户端设计中,账号应该只允许在他们的个人任务列表中操作。

3 任务分配处理器(AssignmentHandler)

可以通过开发 AssignmentHandler 来计算任务的分配者和候选者。

任务分配处理器要实现 AssignmentHandler 接口:

public interface AssignmentHandler extends Serializable {
  // 在 assignable 对象中设置分配者和候选者
  /** sets the actorId and candidates for the given task. */
  void assign(Assignable assignable, OpenExecution execution) throws Exception;
}

注意:Assignable 类型是任务和泳道的通用接口。所以任务分配处理器既可以作为任务活动的元素,又可以作为泳道元素(下面会讲到)的子元素。

任务分配处理器的流程示例

jPDL:

<?xml version="1.0" encoding="UTF-8"?>

<process key="TaskAssignmentHandler" name="TaskAssignmentHandler" xmlns="http://jbpm.org/4.4/jpdl">
   <start g="168,181,48,48" name="start1">
      <transition to="审阅"/>
   </start>
   <task g="307,179,92,52" name="审阅">
   	<assignment-handler class="net.deniro.jbpm.test.task.AssignTask">
   		<field name="assignee">
   			<string value="deniro"/>
   		</field>
   	</assignment-handler>
      <transition to="等待"/>
   </task>
   <state g="456,182,92,52" name="等待"/>
</process>

AssignTask 任务分配处理器:

public class AssignTask implements AssignmentHandler {

    /**
     * jPDL 定义中注入的
     */
    String assignee;

    @Override
    public void assign(Assignable assignable, OpenExecution execution) throws Exception {
        assignable.setAssignee(assignee);
    }
}

assign 方法的 execution 参数对象可以获得流程上下文和变量,所以可以结合其它的 API 来计算出任务的分配者和候选者。

测试代码:

//发起实例
ProcessInstance processInstance = executionService.startProcessInstanceByKey
		("TaskAssignmentHandler");

//出现在 deniro 的已分配任务列表中
final List<Task> taskList = taskService.findPersonalTasks("deniro");
assertEquals(1, taskList.size());//断言有一个任务

Task task = taskList.get(0);
assertEquals("审阅", task.getName());
assertEquals("deniro", task.getAssignee());

4 任务泳道

在实际的业务应用中,经常会遇到这样一种场景:流程定义中的多个任务需要被分配或候选给同一组用户。那么我们可以统一将这个 “同一组的用户” 定义为 “一个泳道”。泳道作为流程定义的直接子元素被整个流程定义所知,因此同一流程定义中的任何一个任务都可以引用泳道。属于同一个泳道的任务将会被分配或者候选给这个泳道中的所有用户。

泳道示例

泳道可以理解为流程定义的 “全局用户组”,它可以被当做一个流程规则来使用。

属性类型默认值是否必需描述
swimlane泳道名称字符串可选引用一个在流程中定义的泳道

swimlane 属性是任务活动对泳道的引用,泳道本身是作为 process 流程定义的子元素被定义在整个流程范围内的。

泳道(swimlane)元素的属性:

属性类型默认值是否必需描述
name泳道名称字符串必需这个名词将在任务的泳道属性中被引用。
assignee表达式可选引用的单个用户ID
candidate-groups表达式可选使用逗号分隔的用户组 ID 列表,此组中的所有用户将作为引用此泳道任务的候选人。
candidate-users表达式可选使用逗号分隔的用户 ID 列表,此列表中的所有用户将作为引用此泳道任务的候选人。

泳道示例流程

jPDL:

<?xml version="1.0" encoding="UTF-8"?>

<process key="TaskSwimlane" name="TaskSwimlane" xmlns="http://jbpm.org/4.4/jpdl">
	<swimlane candidate-groups="sales-dept" name="sales representative"/>
   <start g="188,247,48,48" name="start1">
      <transition to="输入订单数据"/>
   </start>
   <task g="327,245,132,52" name="输入订单数据" swimlane="sales representative">
      <transition to="计算定额"/>
   </task>
   <task g="524,244,153,52" name="计算定额" swimlane="sales representative"/>
</process>

测试代码:

//创建用户并加入组
identityService.createGroup("sales-dept");
identityService.createUser("deniro", "deniro", "li");
identityService.createMembership("deniro", "sales-dept");

//发起实例
ProcessInstance processInstance = executionService.startProcessInstanceByKey
		("TaskSwimlane");


final List<Task> tasks = taskService.findGroupTasks("deniro");//deniro
// 是这个泳道所在组中的唯一候选者
assertFalse(tasks.isEmpty());
String taskId = tasks.get(0).getId();
taskService.takeTask(taskId, "deniro");//接受任务
taskService.completeTask(taskId);//完成任务

List<Task> taskList = taskService.findPersonalTasks("deniro");
assertEquals(1, taskList.size());//断言 deniro 拿到一个任务
Task task = taskList.get(0);
assertEquals("计算定额", task.getName());
assertEquals("deniro", task.getAssignee());

5 任务变量

任务可以读取、更新流程变量。也可以定义任务自由的变量,即任务变量。一般来说,任务变量的主要作用是作为任务表单的数据容器 – 任务表单负责展示来自任务和流程的变量数据;同时用户通过任务表单录入的数据则会被设置为任务变量,任务变量根据需要也可以被输出成为流程变量。

假设有这样一个流程定义:

任务变量流程示例

jPDL:

<?xml version="1.0" encoding="UTF-8"?>

<process key="TaskVariables" name="TaskVariables" xmlns="http://jbpm.org/4.4/jpdl">
   <start g="241,192,48,48" name="start1">
      <transition to="审核"/>
   </start>
   <task assignee="deniro" g="372,189,92,52" name="审核">
      <transition to="等待"/>
   </task>
   <state g="533,187,92,52" name="等待"/>
</process>

测试代码:

//发起实例
ProcessInstance processInstance = executionService.startProcessInstanceByKey
		("TaskVariables");


final List<Task> tasks = taskService.findPersonalTasks("deniro");//deniro
String taskId = tasks.get(0).getId();

//获取任务变量集
Set<String> variableNames = taskService.getVariableNames(taskId);

//取得所有任务变量
Map<String, Object> variables = taskService.getVariables(taskId, variableNames);

//新增或更新某个任务变量,任务变量支持任何可序列化的 Java Object
variables.put("category", "book");
variables.put("num", 1000);

//任务变量放入任务中(持久化操作)
taskService.setVariables(taskId, variables);

6 任务提醒邮件

可以为任务的分配者设置电子邮件提醒:

  • 当一个任务出现在某个任务列表中时立即提醒。
  • 指定时间间隔进行反复提醒。

电子邮件的内容是根据模板生成的,默认使用 jBPM 内置的模板。

任务活动中与电子邮件相关元素:

元素数目描述
notification0…1当一个任务被分配时立即发送一封提醒邮件。如果没有指定模板,邮件会使用默认的 task-notification 模板。
reminder0…1根据指定的时间间隔发送提醒邮件。如果没有指定模板,邮件会使用默认的 task-reminder 模板。

上述的模板都定义在 jbpm.mail.templates.xml 中。

task-notification 模板定义如下:

<mail-template name='task-notification'>
  <to users="${task.assignee}"/>
  <subject>${task.name}</subject>
  <text><![CDATA[Hi ${task.assignee},
Task "${task.name}" has been assigned to you.
${task.description}

Sent by jBPM]]></text>
</mail-template>

task-reminder 模板定义如下:

<mail-template name="rectify-template">
  <to addresses="${addressee}" />
  <cc users="bb" groups="innerparty" />
  <bcc groups="thinkpol" />
  <subject>rectify ${newspaper}</subject>
  <text>${newspaper} ${date} ${details}</text>
</mail-template>

notification 元素的属性:

属性类型默认值是否必需描述
continue字符串枚举 {sync、async、exclusive}sync可选以同步、同步还是独占模式发送 notification 提醒邮件。

reminder 元素的属性:

属性类型默认值是否必需描述
duedate延迟时间(可包含表达式的字符串)必需reminder 提醒电子邮件在任务产生后延迟多少时间后发送。
repeat间隔时间(可包含表达式的字符串)可选reminder 提醒电子邮件每隔多少时间再发送一次,直到任务被办理。
continue字符串枚举 {sync、async、exclusive}sync可选以同步、同步还是独占模式发送 reminder 提醒邮件。

以后我们会 jBPM 的邮件能力进行详细介绍,敬请期待哦O(∩_∩)O~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1.JPDL的流程定义元素 1)第一层:GraphElement 这个容易理解,因为在画流程定义时,每个拖拉的对象都是一个graph的元素。GraphElement有四个属性: (1)processDefine 表示当前元素属于哪个流程定义 (2)events 表示可以接收哪些event (3)name 名字 (4)exceptionHandlers 异常处理类集合(List) 2)第二层:node、processDefinition、Transition、Task 它们都继承自GraphElement (1)processDefinition表示流程定义(implements NodeCollection),它有下面的属性:name、version、nodes、startState。nodes表示流程中所有的node,startState用于启动流程时找到首节点。 (2)Transition表示转移,它有三个属性:from(Node),to(Node),supportedEventTypes表示支持的event类型 (3)node表示节点,它有四个属性:leaving transitions、arriving transitions、action、superState。 (4)Task 定义任务 3)第三层:各种不同的node 它们都继承自node。 Decision、EndState、Fork、Join、Merge、Milestone、 InterleaveEnd、InterleaveStart、ProcessState、State。 2.jBPM的token jbpm中最重要的概念,应该是令牌(Token)和信令(Signal)。在整个流程实例运行过程中,我们可以迅速的利用token得到其当前的current state。在解决“并行”等(比如Fork)问题时,jBpm让Token对象维护了父子关系,这种关系在涉及到Fork的时候会产生。 jBpm让Token这个对象身兼了多种使命: (1)快速定位current state (2)用于fork,join算法 (3)用于告知任务执行者的任务索引。 如下代码: //pd是process definition,pi是process instance ProcessInstance pi = new ProcessInstance( pd ); //得到根令牌 Token token = pi.getRootToken(); //发信令 token.signal(); Token的signal方法也可以传入transition参数,这个方法把信令发送给Token,这样,令牌将被激活,并沿指定的transition离开当前的状态(如果没有指定transition,将沿缺省的transition 离开当前状态)。 jbpm是怎么实现的呢?其实很简单: 1)Token记录了当前的状态(current state),只有当前的状态(或称节点)拥有该令牌 2)向TOKEN发signal后,当前状态收到该signal 3)当前状态把令牌传给signal中指定的transition 4)transition收到令牌后,不强占,马上把令牌传给下个状态. 5)根据令牌的位置,流程的状态已经发生改变. 3.process definition 一个process definition代表了一个正式的业务流程,它以一个流程图为基础。这个流程图由许多node和transition组成。每个node在这个流程图里都有着各自特殊的类型,这些不同的类型决定了node在运行时的不同行为。一个process definition只有一个start state 。 4.token 一个token代表了一条执行路径,它包含了这条执行路径的当前的执行状态(current state)。 5.process instance 一个process instance(流程实例)即一个process definition(流程定义)的流程执行实例。一个process definition可以对应多个process instance。当一个process instance被创建的时候,一个主执行路径token同时被创建,这个token叫做root token,它指向流程定义的start state(processDefinition.getStartState()==token.getNode())。 6.signal 一个signal 发送给token通知token 继续流程的执行。如果signal 没有指定transition,token将沿缺省的transition离开当前状态,如果signal 指定transition,token将沿指定的transition离开当前的状态。看源代码可以看到发给process instance的signal 其实都是发送给了root token。 7.Actions jbpm提供了灵活的action ,当流程执行,token 进入node和transition时,会触发相应的一些event(事件)。在这些event上附上我们自己写的action,就会带动action 的执行。action里是我们自己的相关java操作代码,非常方便。注意的是event(事件)是内置的,无法扩展。另外,action也可以直接挂在node上,而不依赖于event(事件)的触发,这个很重要。 8.node 一个流程图由许多node和transition组成。每个node都有一种类型,这个类型决定了当流程执行到这个node时的不同行为。jbpm有一组node type可以供你选择,当然你可以定制自己node 。 node的作用 node有两个主要的作用: 1)执行java代码,比如创建task instance(任务实例)、发出通知、更新数据库等等。很典型的就是在node 上挂上我们的action 2) 控制流程的执行: A、等待状态:流程进入到这个node时将处于等待状态,直到一个signal 的发出 B、流程将沿着一个leaving transition越过这个node,这种情况特殊一点,需要有个action挂在这个node上(注意这个action不是event触发的!),action中将会调用到API里 executionContext.leaveNode(String transitionName),transitionName即这里的leaving transition名字。 C、创建新的执行路径: 很典型的就是fork node。流程在这里会分叉,产生新的执行路径。这样就创建了新的token,每个新的token代表一个新的执行路径。注意的是,这些新的token和产生前的token是父子关系! D、结束执行路径:一个node可以结束一条执行路径,这同样意味着相应的token的结束和流程的结束。 9.流程图中的node type 1)task-node 一个task-node可以包含一个或多个task,这些task分配给特定的user。当流程执行到task-node时,task instance将会被创建,一个task对应一个task instance。task instances 创建后,task-node就处于等待状态。当所有的task instances被特定的user执行完毕后,将会发出一个新的signal 到token,即流程继续执行。 2)state state是一个纯粹的wait state(等待状态)。它和task-node的区别就是它不会创建task instances。很典型的用法是,当进入这个节点时(通过绑定一个action到node-enter event),发送一条消息到外部的系统,然后流程就处于等待状态。外部系统完成一些操作后返回一条消息,这个消息触发一个signal 到token,然后流程继续执行。(不常用) 3)decision 当需要在流程中根据不同条件来判断执行不同路径时,就可以用decision节点。两种方法:最简单的是在transitions里增加condition elements(条件),condition是beanshell script写的,它返回一个boolean。当运行的时候,decision节点将会在它的 leaving transitions里循环,同时比较 leaving transitions里的condition,最先返回'true'的condition,那个leaving transitions将会被执行;作为选择,你可以实现DecisionHandler接口,它有一个decide()方法,该方法返回一个String(leaving transition的名字)。 4)fork fork节点把一条执行路径分离成多条同时进行(并发)的执行路径,每条离开fork节点的路径产生一个子token。 5)join 默认情况下,join节点会认为所有到达该节点的token都有着相同的父token。join 节点会结束每一个到达该节点的token,当所有的子token都到达该节点后,父token会激活。当仍然有子token处于活动状态时,join 节点是wait state(等待状态)。 6)node node节点就是让你挂自己的action用的(注意:不是event触发!),当流程到达该节点时,action会被执行。你的action要实现ActionHandler接口。同样,在你的action里要控制流程。 10. Actions的明 存在两种action,一种是 event触发的action,一种是挂在node 节点的action。要注意它们的区别,event触发的action无法控制流程,也就是它无法决定流程经过这个节点后下一步将到哪一个leaving transition;而挂在node 节点的action就不同,它可以控制流程。不管是哪一种action都要实现ActionHandler接口。 11. Task任务jbpm一个相当重要的功能就是对任务进行管理。Task任务)是流程定义里的一部分,它决定了task instance的创建和分配。Task任务)可以在task-node节点下定义,也可以挂在process-definition节点下。最普遍的方式是在task-node节点下定义一个或多个任务。默认情况下,流程task-node节点会处于等待状态,直到所有的任务被执行完毕。任务的名称在整个流程中必须是唯一的。一个TaskNode对应多个Task。 对于这样的流程定义: xml 代码 1. <task-node name='a'> 2. <task name='laundry' /> 3. <task name='dishes' /> 4. <task name='change nappy' /> 5. <transition to='b' /> 6. </task-node> 只有当节点中的三个任务都完成后,流程才进入后面的节点 对于这样的流程定义: xml 代码 1. <task-node name='a' signal='first'>> 2. <task name='laundry' /> 3. <task name='dishes' /> 4. <task name='change nappy' /> 5. <transition to='b' /> 6. </task-node> 当第一个任务完成后,token就指向后面的节点 对于这样的流程定义: xml 代码 1. <task-node name='a' signal='never'>> 2. <task name='laundry' /> 3. <task name='dishes' /> 4. <task name='change nappy' /> 5. <transition to='b' /> 6. </task-node> 三个任务都完成后,token仍然不会指向后面的节点;需要自己手动调用processInstance.signal()才会驱动流程到下面的节点。 对于这样的流程定义: xml 代码 1. <task-node name='a' signal='unsynchronized'>> 2. <task name='laundry' /> 3. <task name='dishes' /> 4. <task name='change nappy' /> 5. <transition to='b' /> 6. </task-node> token不会在本节点停留,而是直接到后面的节点 12. jbpm任务管理实现 一个Task instance(任务实例)可以被分配给一个actorId (java.lang.String)。所有的Task instance都被保存在数据库中的表jbpm_taskinstance里。当你想得到特定用户的任务清单时,你就可以通过一个与用户关联的actorId来查询这张表。 一个流程定义有一个TaskMgmtDefinition;一个TaskMgmtDefinition对应多个swimlane,同时对应多个task;一个swimlane有多个task,可以从TaskMgmtDefinition中通过task的名称直接获取相应的task; swimlane对象有四个属性,分别是name(名字)、assignmentDelegation(分配代理类)、taskMgmtDefinition、tasks(Set 对应多个task),可以增加task task对象主要的属性:taskMgmtDefinition、swimlane、assignmentDelegation、taskNode,需要注意的是swimlane和assignmentDelegation中间只是可以一个属性有值,因为它们都和任务的分配有关系。 一个流程实例有一个TaskMgmtInstance;一个TaskMgmtInstance对应多个swimlaneInstance,同时对应多个taskInstance;一个swimlaneInstance有多个taskInstance,可以从TaskMgmtInstance中直接获取相应的taskInstance; swimlaneInstance对象主要有五个属性,分别是name、actorId、pooledActors(Set)、swimlane、taskMgmtInstance。 taskInstance对象的主要属性:name、actorId、task、swimlaneInstance、taskMgmtInstance、pooledActors。 当对任务进行分配时,一般需要实现AssignmentHandler这个接口,这个接口的方法只有一个: void assign( Assignable assignable, ExecutionContext executionContext) throws Exception; 一个典型的实现(把名字是'change nappy'的任务交给NappyAssignmentHandler这个类来分配) xml 代码 1. <task name='change nappy'> 2. <assignment class='org.jbpm.tutorial.taskmgmt.NappyAssignmentHandler' /> 3. task> NappyAssignmentHandler类: java 代码 1. public void assign(Assignable assignable, ExecutionContext executionContext) { 2. assignable.setActorId("papa"); 3. } 同样,Assignable只是一个接口,它有两个方法:setActorId()和setPooledActors(),Assignable的具体实现类也是两个:swimlaneInstancehe和taskInstance。这样就不不难理解整个任务分配流程了: 1、流程进入TaskNode节点,执行TaskNode类的execute()方法,该方法首先获得TaskMgmtInstance实例,然后通过它来创建TaskInstance。taskMgmtInstance.createTaskInstance(task, executionContext); 2、在上面的createTaskInstance(task, executionContext)里,该方法调用了taskInstance.assign(executionContext)对taskInstance进行分配。 3、在assign(executionContext)方法里,首先会判断task属性里是否存在swimlane,如果有的话,这个taskInstance就会分配给swimlane指定的ActorId或 PooledActors;如果不存在,再去找task属性里 assignmentDelegation(分配代理类)通过代理类(即我们自己写的实现AssignmentHandler这个接口的类)指定ActorId或 PooledActors。 13. jbpm的用户角色管理 jbpm在用户角色管理上共设计了四个类:Entity、Membership、Group、User。 Entity类是其他三个类的父类,它包含了两个属性:name(String)、permissions(Set); User类继承Entity类,包含三个属性:password(String)、email(String)、memberships(Set); Group类继承Entity类,包含四个属性: type(String)、parent(Group)、children(Set)、memberships(Set); Membership类继承Entity类,包含三个属性:role(String)、user(User)、group(Group) 很明显,一个user对应一个用户,一个group对应一个用户组,它们之间通过membership关联,并且一个user可以属于多个不同类型(type)的group,user和 group之间是多对多的关系。Membership类的role属性个人感觉用途不大,反倒是name属性代表了user在group里的role(角色)。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值