Activiti——流程任务TaskService API详解

Activiti流程任务

在Activiti中,一个Task实例表示流程中的一个任务,任务类型多种多样,例如用户任务、脚本任务等。TaskService提供了许多操作流程任务的API,包括任务的查询、创建与删除、权限设置和参数设置等。得到ProcessEngine的实例后,可以调用getTaskService方法来获取TaskService实例。

1、任务的创建与删除

一般情况下,可以通过定义流程描述XML文件来定义一个任务,Activiti在解析该文件时,会将任务写到对应的数据表(ACT_RU_TASK)中。在此过程中,创建任务的工作已由Activiti完成了。如果需要使用任务数据,则可以调用相应查询的API查询任务数据并且进行相应的设置。下面将讲解如何使用XML文件定义任务,以及如何使用TaskService提供的API来保存和删除任务数据。

1.1、Task接口

个Task实例表示流程中的一个任务,与其他实例一样,Task是一个接口,并且遵守数据映射实体的命名规范。Task的实现类为TaskEntityImpl,对应的数据库表为ACT_RU_TASK。

TaskEntityImpl包括以下映射属性。

  • id:主键,对应D字段。
  • revision:该数据版本号,对应REV字段。
  • owner::任务拥有人,对应OWNER字段。
  • assignee:被指定需要执行任务的人,对应ASSIGNEE字段。
  • delegationState:任务被委派的状态,对应DELEGATION字段。
  • parentTaskId:父任务的ID(如果本身是子任务的话),对应PARENT_TASK_ID_字段。
  • name:任务名称,对应NAME字段。
  • description:任务的描述信息,对应DESCRIPTION字段。
  • priority:任务的优先级,默认值为50,表示正常状态,对应PRIORITY字段。
  • createTime:任务创建时间,对应CREATE_TIME字段。
  • dueDate:预订日期,对应DUE_DATE字段。
  • executionId:该任务对应的执行流D,对应EXECUTION ID字段。
  • processDefinitionId:任务对应的流程定义D,对应PROC_DEF_ID_字段。
  • claimTime:任务的提醒时间,对应CLAIM_TIME字段。

1.2、创建与保存Task实例

与创建用户组实例(Group)、用户实例(User)一样,TaskService提供了创建Task实例的方法。调用TaskService的newTask())与new Task(String taskId)方法,可以获取一个Task实例开发人员不需要关心Task的创建细节。调用这两个创建Task实例的方法时,TaskService会初始化Task的部分属性,这些属性包括taskId、创建时间等。

创建了Task实例后,如果需要将其保存到数据库中,则可以使用TaskService的saveTask(Task task)方法,如果保存的Task实例有ID值,则会使用该值作为Task数据的主键,没有的话,则由Activiti为其生成主键。

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
//保存第一个Task,不设置ID
Task task1 = taskService.newTask();
taskService.saveTask(task1);
//保存第二个task,设置ID
Task task2 = taskService.newTask("审核任务");
taskService.saveTask(task2);

在这里插入图片描述

1.3、删除任务

TaskService提供了多个删除任务的方法,包括删除单个任务、删除多个任务的方法。由开发人员决定是否进行级联
删除。这些删除方法描述如下。

  • deleteTask(String taskId):根据Task的ID删除Task数据,调用该方法不会进行级联删除。
  • deleteTask(String taskId,boolean cascade):根据Task的ID删除Task数据,由调用者决定是否进行级联删除。
  • deleteTasks(CollectiontaskIds):提供多个Task的ID进行多条数据删除,调用该方法不会进行级联删除。
  • deleteTasks(Collection-taskIds,boolean cascade)'提供多个Task的ID进行多条数据删除,由调用者决定是否进行级联删除。

删除任务时,将会删除该任务下面全部的子任务和该任务本身,如果设置了进行级联删除,则会删除与该任务相关的全部历史数据(ACT_HI_TASKINST表)和子任务,如果不进行级联删除,则会使用历史数据将该任务设置为结束状态。除此之外,如果尝试删除一条不存的任务数据(提供不存在的taskId),此时deleteTask方法会到历史数据表中查询是否存在该任务相关的历史数据,如果存在则删除,不存在则忽略。

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
//保存多个Task
for (int i = 1; i < 10; i++) {
    Task t = taskService.newTask(String.valueOf(i));
    taskService.saveTask(t);
}
//删除task(不包括历史数据和子任务)
taskService.deleteTask("1");
//删除task(包括历史数据和子任务)
taskService.deleteTask("2", true);
//删除多个task(不包括历史数据和子任务)
List<String> ids = new ArrayList<String>();
ids.add("3");
ids.add("4");
taskService.deleteTasks(ids);
//阐述多个task(包括历史数据和子任务)
ids = new ArrayList<String>();
ids.add("5");
ids.add("6");
taskService.deleteTasks(ids, true);

在这里插入图片描述

2、任务权限

Activiti对流程定义的权限不做任何的控制,只会提供相应的流程定义查询方法,将相应的流程定义数据展现给不同的角色。任务的权限同样使用这种设计,Activiti不会对权限进行拦截,只提供查询的API让开发人员使用。

2.1、设置候选用户组

流程定义与用户组(或者用户)之间的权限数据,通过一个ACT_RU_IDENTITYLINK中间表来保存,该表对应的实体为IdentityLink对象。任务的权限数据设置与之类似,也是使用ACT RU IDENTITYLINK表来保存这些权限数据,因此在调用设置流程权限API时,Activiti最终会往这个表中写入数据。

@Test
public void setGroup() {
    ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
    TaskService taskService = processEngine.getTaskService();
    IdentityService identityService = processEngine.getIdentityService();
    //新建用户组
    Group groupA = createGroup(identityService, "group1", "经理组", "manager");
    //保存第一个Task
    Task task1 = taskService.newTask("task1");
    taskService.saveTask(task1);
    //保存第二个Task
    Task task2 = taskService.newTask("task2");
    taskService.saveTask(task2);
    //绑定用户组与任务的关系
    taskService.addCandidateGroup("task1", groupA.getId());
    taskService.addCandidateGroup("task2", groupA.getId());
}

private Group createGroup(IdentityService identityService, String id, String name, String type) {
    Group group = identityService.newGroup(id);
    group.setName(name);
    group.setType(type);
    identityService.saveGroup(group);
    return identityService.createGroupQuery().groupId(id).singleResult();
}

在这里插入图片描述

ACT_RU_IDENTITYLINK表中被加入了两条数据,其中TASK_ID字段为相应任务数据的ID,GROUP_ID为用户组数据的D,另外,TYPE字段值为“cadidate”,表示该任务为控制请求的权限数据。

在使用addCandidateGroup方法时,需要注意的是,如果给出用户组ID为null,那么将会抛出异常,异常信息为:userld and groupld cannot both be null;如果给出的任务D对应的数据不存在,同样也会抛出异常,异常信息如下:Cannot find task with id(ID)。

2.2、设置候选用户

候选用户是一“群”将会拥有或者执行任务权限的人,但是任务只允许一个人执行或者拥有,而任务的候选人则是指一个用户群体。TaskService同样提供了一个设置用户权限数据的方法addCandidateUser,与addCandidateGroup方法类似,调用该方法需要提供用户ID与任务ID,执行该方法后,会向ACT_RU_IDENTITYLINK表中写入相应的权限数据。

@Test
public void setUser() {
    ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
    TaskService taskService = processEngine.getTaskService();
    IdentityService identityService = processEngine.getIdentityService();
    //新建用户
    User user1 = createUser(identityService, UUID.randomUUID().toString(), "张三", "lastname", "abc@qq.com", "123");
    User user2 = createUser(identityService, UUID.randomUUID().toString(), "李四", "lastname", "abc@qq.com", "123");
    //保存一个Task
    Task task = taskService.newTask("task");
    taskService.saveTask(task);
    //绑定关系
    taskService.addCandidateUser("task", user1.getId());
    taskService.addCandidateUser("task", user2.getId());
}

private User createUser(IdentityService identityService, String id, String first, String last, String email, String passwd) {
    User user = identityService.newUser(id);
    user.setFirstName(first);
    user.setLastName(last);
    user.setEmail(email);
    user.setPassword(passwd);
    identityService.saveUser(user);
    return identityService.createUserQuery().userId(id).singleResult();
}

在这里插入图片描述

2.3、权限数据查询

如果需要查询用户组和用户的候选任务,可以使用TaskService的createTaskQuery方法,得到Task对应的查询对象。TaskQuery中提供了taskCandidateGroup和taskCandidateUser方法,这两个方法可以根据用户组或者用户的ID查询候选Task的数据。与流程定义一样,这些查询方法会先到权限数据表中查询与用户组或者用户关联了的数据,查询得到taskId后,再到Task中查询任务数据并且返回。如果得到了任务的ID,想查询相应的关系数据,则可以调用TaskService的getIdentityLinksFor Task方法,该方法根据任务ID查询IdentityLink集合。

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
IdentityService identityService = processEngine.getIdentityService();
//新建用户
User user = createUser(identityService, "user1", "张三", "last", "abc@qq.com", "123");
//新建用户组
Group groupA = createGroup(identityService, "group1", "经理组", "manager");
Group groupB = createGroup(identityService, "group2", "员工组", "employee");
//保存第一个Task
Task task1 = taskService.newTask("task1");
task1.setName("申请假期");
taskService.saveTask(task1);
//保存第二个Task
Task task2 = taskService.newTask("task2");
task2.setName("审批假期");
taskService.saveTask(task2);
//绑定权限
taskService.addCandidateGroup("task1", groupA.getId());
taskService.addCandidateGroup("task2", groupB.getId());
taskService.addCandidateUser("task2", user.getId());
//根据用户组查询任务
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup(groupA.getId()).list();
System.out.println(tasks);
//根据用户查询任务
tasks = taskService.createTaskQuery().taskCandidateUser(user.getId()).list();
System.out.println(tasks);
//taskCandidateGroupIn
List<String> groupIds = new ArrayList<String>();
groupIds.add(groupA.getId());
groupIds.add(groupB.getId());
tasks = taskService.createTaskQuery().taskCandidateGroupIn(groupIds).list();
System.out.println(tasks);
//查询权限数据
List<IdentityLink> links = taskService.getIdentityLinksForTask(tasks.get(0).getId());
System.out.println(links);

2.4、设置任务持有人

TaskService中提供了一个setOwner方法来设置任务的持有人,调用该方法后,会设置流程表的OWNER字段为相应用户的ID。如果想根据用户查询其所拥有的任务,可以调用
TaskQuery的taskOwner方法。

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
IdentityService identityService = processEngine.getIdentityService();
User user = createUser(identityService, "u1", "张三", "last", "abc@qq.com", "123");
Task task1 = taskService.newTask("t1");
task1.setName("申请任务");
taskService.saveTask(task1);
//设置任务持有人
taskService.setOwner(task1.getId(), user.getId());
System.out.println("用户张三持有的任务数量:" + taskService.createTaskQuery().taskOwner(user.getId()).count());

2.5、设置任务代理人

除设置任务持有人外,TaskService还提供了一个setAssignee方法用于设置任务的代理人,与setOwner方法一样,setAssignee方法会改变ACT_RU_TASK表的ASSIGNEE字段值。当需要根据任务代理人查询任务时,可以调用TaskQuery的taskAssignee方法设定该查询条件。

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
IdentityService identityService = processEngine.getIdentityService();
User user = createUser(identityService, "u11", "张三", "last", "abc@qq.com", "123");
Task task1 = taskService.newTask("t11");
task1.setName("申请任务");
taskService.saveTask(task1);
//设置任务持有人
taskService.setAssignee(task1.getId(), user.getId());
System.out.println("用户张三持有的任务数量:" + taskService.createTaskQuery().taskOwner(user.getId()).count());

2.6、添加任务权限数据

TaskService还提供了两个添加任务权限数据的方法,这两个方法的描述如下。

  • addGroupldentityLink(String taskId,String groupld,String identityLinkType):添加用户组
    权限数据,第一个参数为任务D,第二个参数为用户组D,第三个参数为权限数据类型标识。
  • addUserIdentityLink(String taskId,String userld,String identityLinkType):添加用户权限数据,第一个参数为任务ID,第二个参数为用户ID,第三个参数为权限数据类型标识。
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
IdentityService identityService = processEngine.getIdentityService();
Group groupA = createGroup(identityService, "g1", "经理组", "manager");
User user = createUser(identityService, "uuu1", "张三", "last", "abc@qq.com", "123");
Task task1 = taskService.newTask("ttt1");
task1.setName("申请任务");
taskService.saveTask(task1);
//addCroupIdentityLink
taskService.addGroupIdentityLink(task1.getId(), groupA.getId(), IdentityLinkType.CANDIDATE);
taskService.addGroupIdentityLink(task1.getId(), groupA.getId(), IdentityLinkType.OWNER);
taskService.addGroupIdentityLink(task1.getId(), groupA.getId(), IdentityLinkType.ASSIGNEE);
//addUserIdentityLink
Task task2 = taskService.newTask("ttt2");
task2.setName("申请任务2");
taskService.saveTask(task2);
taskService.addUserIdentityLink(task2.getId(), user.getId(), IdentityLinkType.CANDIDATE);
taskService.addUserIdentityLink(task2.getId(), user.getId(), IdentityLinkType.OWNER);
taskService.addUserIdentityLink(task2.getId(), user.getId(), IdentityLinkType.ASSIGNEE);

代码分别调用addGroupIdentityLink和addUserIdentityLink方法三次,分别将第三个参数(权限类型标识)设置为CANDIDATE、OWNER和ASSIGNEE。

使用addUserIdentityLink方法将权限类型标识设置为CANDIDATE,其效果等同于调用addCandidateUser方法;将权限类型标识设置为OWNER,其效果等同于调用setOwner方法;将权限类型标识设置为ASSIGNEE,其效果等同于调用setAssignee方法。调用addGroupIdentityLink方法与调用addUserIdentityLink方法的效果类似,但是需要注意的是,将用户组设置为任务所有人或者任务代理人,这并不合适,虽然可以成功调用addGroupIdentityLink
方法,但其在删除权限数据时,将会抛出异常。

2.7、删除用户组权限

TaskService中提供了两个方法用于删除用户组的任务权限,这两个方法的描述如下。

  • deleteGroupIdentityLink(String taskId,String groupld,String identityLinkType):删除任务的权限数据,第一个参数为任务ID,第二个参数为用户组ID,第三个参数为任务权限类型标识。
  • deleteCandidateGroup(String taskId,String groupld):删除任务的候选用户组数据,第一个参数为任务ID,第二个参数为用户组ID。
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
IdentityService identityService = processEngine.getIdentityService();
Group g = createGroup(identityService, "groupppp", "经理组", "manager");
Task t1 = taskService.newTask("taskkk");
t1.setName("审批任务");
taskService.saveTask(t1);
//addGroupIdentityLink
taskService.addGroupIdentityLink(t1.getId(), g.getId(), IdentityLinkType.CANDIDATE);
taskService.addGroupIdentityLink(t1.getId(), g.getId(), IdentityLinkType.OWNER);
taskService.addGroupIdentityLink(t1.getId(), g.getId(), IdentityLinkType.ASSIGNEE);
//删除
taskService.deleteCandidateGroup(t1.getId(), g.getId());
//以下两个方法将抛出异常
taskService.deleteGroupIdentityLink(t1.getId(), g.getId(), IdentityLinkType.OWNER);
taskService.deleteGroupIdentityLink(t1.getId(), g.getId(), IdentityLinkType.ASSIGNEE);

2.8、删除用户权限

TaskService中提供了两个类似的方法用于删除用户的任务权限数据,这两个方法的描述如下。

  • deleteCandidateUser(String taskId,String userld):删除任务权限类型标识为“CANDIDATE”的用户权限数据。
  • deleteUserIdentityLink(String taskId,String userld,String identityLinkType):删除任务权限类型为identityLinkType的用户权限数据。

在使用addCandidateUser方法时,会插入一条权限数据到权限中间表中,如果需要删除该权限数据,则可以调用相应的deleteCandidateUser方法。除了addCandidateUser方法外,还有setOwner、setAssignee和addUserIdentityLink方法可以用于添加不同类型的权限数据,其中setOwner和setAssignee方法会改变任务表的OWNER和ASSIGNEE字段值。在调用deleteUserIdentityLink方法时,如果传入的类型标识为OWNER或者ASSIGNEE,那么会将任务表的OWNER和ASSIGNEE字段的值设置为null。

//获取流程引擎实例
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 获取身份服务组件
IdentityService identityService = engine.getIdentityService();
// 新建用户
User user = createUser(identityService, "user1", "first", "last", "abc@163.com", "123");
// 获取任务服务组件
TaskService taskService = engine.getTaskService();
//保存第一个Task
Task task1 = taskService.newTask("task1");
taskService.saveTask(task1);
//添加用户权限
taskService.addCandidateUser(task1.getId(), user.getId());
long count = taskService.createTaskQuery().taskCandidateUser(user.getId()).count();
System.out.println("调用addCandidateUser方法后,用户的候选任务数量:" + count);
//删除用户权限
taskService.deleteCandidateUser(task1.getId(), user.getId());
count = taskService.createTaskQuery().taskCandidateUser(user.getId()).count();
System.out.println("调用deleteCandidateUser方法后,用户的候选任务数量:" + count);
//添加用户权限
taskService.addUserIdentityLink(task1.getId(), user.getId(), IdentityLinkType.OWNER);
count = taskService.createTaskQuery().taskOwner(user.getId()).count();
System.out.println("调用addUserIdentityLink方法后,用户的候选任务数量:" + count);
//删除用户权限
taskService.deleteUserIdentityLink(task1.getId(), user.getId(), IdentityLinkType.OWNER);
count = taskService.createTaskQuery().taskOwner(user.getId()).count();
System.out.println("调用deleteUserIdentityLink方法后,用户的候选任务数量:" + count);

3、任务参数

当一个任务被传递到执行人手中时,他需要知道该任务的全部信息,包括任务的基本信息(创建时间、内容等),还需要得到任务的相关参数。例如一个请假申请,请假的天数、开始时间等均为该申请的参数。编写这个请假申请的任务由请假申请人发起,在执行编写请假任务时,就需要设置这一系列的请假任务参数。在Activiti中,参数类型分为流程参数和任务参数。

3.1、基本类型参数设置

在Activiti数据库设计相关章节中我们讲过,Activiti中的各种参数均保存在ACT_RU_VARIABLE表中,因此当调用了相应的API设置参数后,这些参数都会体现在参数数据表中。Activiti支持多种参数类型设置,开发者可以根据实际情况设置不同的参数。例如在一个请假流程中,若需要设置天数,可以使用Integer类型;需要设置日期,可以使用Date
类型。设置参数可以调用TaskService的set Variable(String taskId,String variableName,Object value)方法。调用该方法需要传入taskId、参数名称和参数值,其中参数值类型为Object,根据传入的参数类型,参数表的TYPE字段会记录参数的类型标识,当前Activiti支持以下基本参数类型。

  • Boolean:布尔类型,参数类型标识为boolean。
  • Date:日期类型,参数类型标识为date。
  • Double:双精度类型,参数类型标识为double。
  • Integer:整型,参数类型标识为integer。
  • Long:长整型,参数类型标识为long。
  • Null:空值,参数类型标识为null。
  • Short:短整型,参数类型标识为short。.
  • String:字符型,参数类型标识为string。
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
//保存第一个Task
Task task1 = taskService.newTask("task1");
taskService.saveTask(task1);
Date d = new Date();
short s = 3;
//设置各种基本类型参数
taskService.setVariable(task1.getId(), "arg0", false);
taskService.setVariable(task1.getId(), "arg1", d);
taskService.setVariable(task1.getId(), "arg2", 1.5D);
taskService.setVariable(task1.getId(), "arg3", 2);
taskService.setVariable(task1.getId(), "arg4", 10L);
taskService.setVariable(task1.getId(), "arg5", null);
taskService.setVariable(task1.getId(), "arg6", s);
taskService.setVariable(task1.getId(), "arg7", "test");

在这里插入图片描述

3.2、序列化参数

setVariable方法的第三个参数的类型为Object,因此在调用该方法时,可以传入自定义对象。如果传入的参数为自定义对象,那么调用setVariable方法时,会将该对象进行序列化(被序列化的对象必须实现Serializable接口),
然后将其保存到资源表(ACT GE BYTEARRAY)中,而参数表为该参数数据做外键关联。

资源表用于保存流程资源或者byte数组,因此可以推测set Variable的实现过程,即该方法会将传入的对象进行序列化,得到bye数组后,将其写入数据库中。

ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
Task t = taskService.newTask(UUID.randomUUID().toString());
t.setName("出差申请");
taskService.saveTask(t);
//设置序列化参数
taskService.setVariable(t.getId(), "arg0", new TestVO("hello"));
public class TestVO implements Serializable {

    public TestVO(String name) {
        this.name = name;
    }

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

3.3、获取参数

在调用setVariable方法后,不管设置的是基本类型的参数还是序列化参数,均可以使用TaskService的getVariable方法得到任务的参数。调用该方法只需要提供任务ID与参数名称即可:

ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
Task task1 = taskService.newTask("ttt1");
task1.setName("请差申请");
taskService.saveTask(task1);
//设置各种基本类型参数
taskService.setVariable(task1.getId(), "days", 5);
taskService.setVariable(task1.getId(), "target", new TestVO("北京"));
//获取天数
Integer days = (Integer)taskService.getVariable(task1.getId(), "days");
System.out.println("出差天数:" + days);
//获取目的地
TestVO target = (TestVO)taskService.getVariable(task1.getId(), "target");
System.out.println("出差目的地:" + target.getName());
出差天数:5
出差目的地:北京

3.4、参数作用域

当任务与流程绑定后,设置的参数均会有其作用域。例如设置一个任务参数,希望在整个流程中均可以使用,那么可以调用setVariable方法,如果只希望该参数仅仅在当前这个任务中使用,那么可以调用TaskService的setVariableLocal方法。调用了setVariable方法后,如果调用getVariableLocal方法来获取参数,将查找不到任何值,因为getVariableLocal方法会查询当前任务的参数,而不会查询整个流程中的全局参数:

<process id="vacationProcess" name="vacation">
    <startEvent id="startevent1" name="Start"></startEvent>
    <userTask id="usertask1" name="Write Vacation"></userTask>
    <endEvent id="endevent1" name="End"></endEvent>
    <sequenceFlow id="flow1" name="" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
    <sequenceFlow id="flow2" name="" sourceRef="usertask1" targetRef="endevent1"></sequenceFlow>
  </process>
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
RuntimeService runtimeService = engine.getRuntimeService();
RepositoryService repositoryService = engine.getRepositoryService();
Deployment dep = repositoryService.createDeployment()
        .addClasspathResource("demo8/vacation.bpmn").deploy();
// 查找流程定义
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery()
        .deploymentId(dep.getId()).singleResult();
// 启动流程
ProcessInstance pi = runtimeService.startProcessInstanceById(pd.getId());
// 分别调用setVariable和setVariableLocal方法
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
taskService.setVariable(task.getId(), "days", 10);
taskService.setVariableLocal(task.getId(), "target", "欧洲");
// 获取参数
Object data1 = taskService.getVariable(task.getId(), "days");
System.out.println("获取休假天数:" + data1);
Object data2 = taskService.getVariable(task.getId(), "target");
System.out.println("获取目的地: " + data2);
// 获取参数
Object data3 = taskService.getVariableLocal(task.getId(), "days");
System.out.println("使用getVariableLocal方法获取天数:" + data3);
获取休假天数:10
获取目的地: 欧洲
使用getVariableLocal方法获取天数:null

3.5、设置多个参数

如果一个任务需要设置多个参数,则可以定义一个序列化对象,将这些参数放到这个对象中,也可以使用TaskService的set Variables和setVariablesLocal方法,传入参数的Map集合,参数Map的key为参数的名称,value为参数值。

ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
//保存第一个Task
Task task1 = taskService.newTask(UUID.randomUUID().toString());
task1.setName("请假流程");
taskService.saveTask(task1);
//初始化参数
Map<String,Object> vars = new HashMap<String, Object>();
vars.put("days", 10);
vars.put("target", "欧洲");
taskService.setVariables(task1.getId(), vars);

实际上set Variables方法最终会遍历参数的Map,然后再逐一设置参数(重用setVariable逻辑)。设置多个参数的set VariablesLocal方法,其逻辑与setVariables方法类似,遍历参数的Map,再逐一设置参数(调用setVariableLocal方法),故setVariablesLocal方法的使用在此不再赘述。

3.6、数据对象

在BPMN文件中,可以使用dataObject元素来定义流程参数。流程启动后,这些参数将会自动被设置到流程实例中,可以使用RuntimeService、TaskService的方法来查询这些参数。

<process id="vacationProcess" name="vacation">
	<!-- 定义了名称,默认值为 Crazyit -->
	<dataObject id="personName" name="personName" itemSubjectRef="xsd:string">
		<extensionElements>
			<activiti:value>Crazyit</activiti:value>
		</extensionElements>
	</dataObject>
	<!-- 定义了年龄,默认值为20 -->
	<dataObject id="personAge" name="personAge" itemSubjectRef="xsd:int">
		<extensionElements>
			<activiti:value>20</activiti:value>
		</extensionElements>
	</dataObject>
	<startEvent id="startevent1" name="Start"></startEvent>
	<userTask id="usertask1" name="Write Vacation"></userTask>
	<endEvent id="endevent1" name="End"></endEvent>
	<sequenceFlow id="flow1" name="" sourceRef="startevent1"
		targetRef="usertask1"></sequenceFlow>
	<sequenceFlow id="flow2" name="" sourceRef="usertask1"
		targetRef="endevent1"></sequenceFlow>
</process>
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
RuntimeService runtimeService = engine.getRuntimeService();
RepositoryService repositoryService = engine.getRepositoryService();
Deployment dep = repositoryService.createDeployment()
        .addClasspathResource("demo8/dataObject.bpmn").deploy();
// 查找流程定义
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().deploymentId(dep.getId()).singleResult();
// 启动流程
ProcessInstance pi = runtimeService.startProcessInstanceById(pd.getId());
// 查询流程任务
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
// 获取全部参数
Map<String, DataObject> objs = taskService.getDataObjects(task.getId());
// 输出参数
for(String key : objs.keySet()) {
    System.out.println(key + "---" + objs.get(key).getValue());
}
personAge---20
personName---Crazyit

4、任务附件管理

在实际生活中,许多流程都会带上一些任务相关的附件,例如报销流程有可能需要附上发票的单据,奖惩流程有可能会附上相关的说明文件。为此,TaskService中提供了用于附件管理的API,通过调用这些API,可以对任务附件进行创建、删除和查询的操作。在Activiti中使用附件表(ACT_HI_ATTACHMENT)保存任务附件数据,与其对应的实体类为AttachmentEntityImpl。

4.1、Attachment对象

一个Attachment实例表示一条任务附件的数据,Attachment接口提供了获取任务附件属性的各个方法,如何设置该对象的属性,完全由TaskService完成,使用者不需要关心设置的过程。该接口的实现类为AttachmentEntityImpl,包括以下属性。

  • id:附件表的主键,对应D字段。
  • revision:附件数据的版本,对应REV字段。
  • name:附件名称,由调用者提供,保存在NAME字段。
  • desciption:附件描述,由调用者提供,保存在DESCRIPTION字段。
  • type:附件类型,由调用者定义,保存在TYPE字段。
  • taskId:该附件对应的任务D,对应TASK_ID字段。
  • processInstanceld:流程实例ID,对应PROC_INST_ID字段。
  • url:附件的URL,由调用者提供,对应URL字段。
  • contentId:附件内容的D,如果调用者提供了输入流作为附件的内容,那么这些内容将会被保存到资源表(ACT_GE_BYTEARRAY)中,该字段将为资源表的外键ID,对应CONTENT_ID字段。

4.2、创建任务附件

TaskService中提供了两个创建任务附件的方法,这两个方法的描述如下。

  • createAttachment(String attachmentType,String taskId,String processInstanceld,String
    attachmentName,String attachmentDescription,String url)
    :创建任务附件,attachmentType为附件类型,由调用者定义;taskId为任务D;processInstanceld为流程实例ID:attachmentName为附件名称;attachmentDescription为附件描述;url为该附件的URL地址。
  • createAttachment(String attachmentType,String taskId,String processInstanceld,String attachmentName,String attachmentDescription,nputStream content):该方法与前一个createAttachment方法一样,只是最后一个参数的类型为InputStream。当调用该方法时,会将最后的输出流参数转换为byte数组,并保存到资源表中,最后设置CONTENT_ID的字段值为资源表中的数据ID。

在此需要注意的是,当调用第一个createAttachment方法时,对于提供的URL,Activiti并不会解析它并生成byte数组,只是单纯地将该URL保存到URL字段中。

ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
RuntimeService runtimeService = engine.getRuntimeService();
RepositoryService repositoryService = engine.getRepositoryService();
Deployment dep = repositoryService.createDeployment()
        .addClasspathResource("demo8/vacation.bpmn").deploy();
// 查找流程定义
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().deploymentId(dep.getId()).singleResult();
// 启动流程
ProcessInstance pi = runtimeService.startProcessInstanceById(pd.getId());
// 查找任务
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
// 设置任务附件
taskService.createAttachment("web url", task.getId(), pi.getId(), "163.com",
        "163 web page", "http://www.163.com");
// 创建图片输入流
InputStream is = new FileInputStream(new File("./src/main/resources/demo8/522.jpg"));
// 设置输入流为任务附件
taskService.createAttachment("web url", task.getId(), pi.getId(), "163.com",
        "163 web page", is);

在这里插入图片描述

4.3、附件查询

TaskService中提供了四个查询附件的方法,这些方法包括根据任务ID查询附件集合、根据流程实例D查询附件集合、根据附件D查询附件数据和根据附件D查询附件内容。以下
为这四个查询附件方法的描述。

  • getProcessInstanceAttachments(String processInstanceld):根据流程实例ID查询该流程实例下全部的附件,返回Attachment集合。
  • getTaskAttachments(String taskId):根据任务ID查询该任务下全部的附件,返回Attachment集合。
  • getAttachment((String attachmentId):根据附件的ID查询附件数据,返回一个Attachment对象。
  • getAttachmentContent(String attachmentld):根据附件的D获取该附件的内容,返回附件内容的输入流对象,如果调用的是第二个createAttachment方法(传入附件的
    InputStream),那么调用该方法才会返回非空的输入流,否则将返回null。
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
RuntimeService runtimeService = engine.getRuntimeService();
RepositoryService repositoryService = engine.getRepositoryService();
// 部署流程描述文件
Deployment dep = repositoryService.createDeployment().addClasspathResource("demo8/vacation.bpmn").deploy();
// 查找流程定义
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().deploymentId(dep.getId()).singleResult();
// 启动流程
ProcessInstance pi = runtimeService.startProcessInstanceById(pd.getId());
// 查找任务
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
// 设置任务附件
Attachment att1 = taskService.createAttachment("web url", task.getId(), pi.getId(), "Attachement1",
        "163 web page", "http://www.163.com");
// 创建图片输入流
InputStream is = new FileInputStream(new File("./src/main/resources/demo8/522.jpg"));
// 设置输入流为任务附件
Attachment att2 = taskService.createAttachment("web url", task.getId(), pi.getId(), "Attachement2",
        "Image InputStream", is);
// 根据流程实例ID查询附件
List<Attachment> attas1 = taskService.getProcessInstanceAttachments(pi.getId());
System.out.println("流程附件数量:" + attas1.size());
// 根据任务ID查询附件
List<Attachment> attas2 = taskService.getTaskAttachments(task.getId());
System.out.println("任务附件数量:" + attas2.size());
// 根据附件ID查询附件
Attachment attResult = taskService.getAttachment(att1.getId());
System.out.println("附件1名称:" + attResult.getName());
// 根据附件ID查询附件内容
InputStream stream1 = taskService.getAttachmentContent(att1.getId());
System.out.println("附件1的输入流:" + stream1);
InputStream stream2 = taskService.getAttachmentContent(att2.getId());
System.out.println("附件2的输入流:" + stream2);
任务附件数量:2
附件1名称:Attachement1
附件1的输入流:null
附件2的输入流:java.io.ByteArrayInputStream@8ab78bc

4.4、删除附件

如果需要删除附件数据,只需要调用TaskService的deleteAttachment(String attachmentId)方法即可。如果在创建附件(调用createAttachment方法)时传入了输入流对象,那么将会在资源表中生成相应的资源数据,在调用deleteAttachment方法时,并不会将相应的资源表的数
据删除,只会删除附件表中的数据及相应的历史数据。

public static void main(String[] args) throws Exception {
	// 获取流程引擎实例
	ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
	// 获取任务服务组件
	TaskService taskService = engine.getTaskService();
	// 获取运行服务组件
	RuntimeService runtimeService = engine.getRuntimeService();
	// 流程存储服务组件
	RepositoryService repositoryService = engine.getRepositoryService();
	// 部署流程描述文件
	Deployment dep = repositoryService.createDeployment()
			.addClasspathResource("bpmn/vacation.bpmn").deploy();
	// 查找流程定义
	ProcessDefinition pd = repositoryService.createProcessDefinitionQuery()
			.deploymentId(dep.getId()).singleResult();
	// 启动流程
	ProcessInstance pi = runtimeService
			.startProcessInstanceById(pd.getId());
	// 查找任务
	Task task = taskService.createTaskQuery().processInstanceId(pi.getId())
			.singleResult();
	// 设置任务附件
	Attachment att1 = taskService.createAttachment("web url", task.getId(), pi.getId(), "Attachement1", 
			"163 web page", "http://www.163.com");
	// 创建图片输入流
	InputStream is = new FileInputStream(new File("resource/artifact/result.png"));
	// 设置输入流为任务附件
	Attachment att2 = taskService.createAttachment("web url", task.getId(), pi.getId(), "Attachement2", 
			"Image InputStream", is);
	System.out.println("删除前数量:" + taskService.getTaskAttachments(task.getId()).size());
	taskService.deleteAttachment(att2.getId());
	System.out.println("删除后数量:" + taskService.getTaskAttachments(task.getId()).size());
}

5、任务评论与事件记录

在日常的工作流程中,随着业务的进行,可能会夹杂着一些个人的流程意见,使用Activiti,可以将任务或者流程的评论保存到评论表(ACT_HI_COMMENT)中,接口为Comment,实现类为CommentEntityImpl。评论表会保存两种类型的数据:任务评论和部分事件记录。

5.1、Comment对象

一个Comment实例表示评论表的一条数据,CommentEntityImpl实际上实现了两个接口:Event和Comment。如果该对象作为Event接口返回,则可以认为它返回的是事件记录的数据,如果该对象作为Comment返回,则其返回的是评论数据。在使用过程中,只可以得到Comment或者Event接口的实例,Comment和Event接口中只定义了一系列的getter方法用于获取相关信息,设置属性的方法,均被放到TaskService中实现。

CommentEntityImpl主要包含以下属性。

  • id:评论表的主键,对应ID字段
  • type:该数据的类型,对应TYPE字段,该属性有两个值,分别为“event’”和“comment”。当值为“event’”时,表示该数据为事件的记录;当值为“comment’”时,表示为任务
    或者流程的评论数据。
  • userId:产生此数据用户的ID,对应USER_ID字段。
  • time:该数据的产生时间,对应TME字段。
  • taskId:该评论(或者事件记录)数据对应的任务D,对应TASK字段。
  • processInstanceld:该评论(或者事件记录)数据对应的流程实例D,对应PROC_INST_ID字段。
  • action:该数据的操作标识,对应ACTION字段。
  • message:该评论(或者事件记录)数据的信息,对应MESSAGE字段。
  • fullMessage:该评论(或者事件记录)数据的信息,对应FULL_MSG字段。

5.2、新增任务评论

新增一个任务评论,可使用TaskService的addComment方法,调用该方法需要传入taskId、流程实例ID和信息参数。

ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
RuntimeService runtimeService = engine.getRuntimeService();
RepositoryService repositoryService = engine.getRepositoryService();
Deployment dep = repositoryService.createDeployment().addClasspathResource("demo8/vacation.bpmn").deploy();
// 查找流程定义
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().deploymentId(dep.getId()).singleResult();
// 启动流程
ProcessInstance pi = runtimeService.startProcessInstanceById(pd.getId());
// 查找任务
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
// 添加任务评论
taskService.addComment(task.getId(), pi.getId(), "this is comment message");
// 查询评论
List<Comment> comments = taskService.getTaskComments(task.getId());
System.out.println("评论数量:" + comments.size());

在这里插入图片描述
可以看到评论表中的评论数据,其中TYPE字段值为“comment’”,表示该条数据为评论数据,并且ACTION字段值为“AddComment”。ACTION字段的值被定义在Event接口中,在调用特定方法时,会向评论表中写入数据,并且为ACTION字段设置相应的值。

5.3、事件的记录

评论表中会保存方法的调用历史记录和任务(流程)评论数据,当调用不同方法时,ACTION字段会被设置为不同的值:

ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
RuntimeService runtimeService = engine.getRuntimeService();
RepositoryService repositoryService = engine.getRepositoryService();
// 部署流程描述文件
Deployment dep = repositoryService.createDeployment().addClasspathResource("demo8/vacation.bpmn").deploy();
// 查找流程定义
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().deploymentId(dep.getId()).singleResult();
// 启动流程
ProcessInstance pi = runtimeService.startProcessInstanceById(pd.getId());
// 查找任务
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
// 调用各个记录事件的方法
taskService.addComment(task.getId(), pi.getId(), "this is comment message");
taskService.addUserIdentityLink(task.getId(), "1", "user");
taskService.deleteUserIdentityLink(task.getId(), "1", "user");
taskService.addGroupIdentityLink(task.getId(), "1", "group");
taskService.deleteGroupIdentityLink(task.getId(), "1", "group");
Attachment atta = taskService.createAttachment("test", task.getId(), pi.getId(), "test", "test", "");
taskService.deleteAttachment(atta.getId());
// 查询Comment和Event
List<Comment> comments = taskService.getTaskComments(task.getId());
System.out.println("总共的评论数量:" + comments.size());
List<Event> events = taskService.getTaskEvents(task.getId());
System.out.println("总共的事件数量:" + events.size());
总共的评论数量:1
总共的事件数量:7

可以看到评论表中产生了若干条数据,其中ACTION字段会出现以下值。

  • AddUserLink:调用TaskService的addUserIdentityLink方法时设置该值。
  • DeleteUserLink:调用TaskService的deleteUserIdentityLink方法时设置该值。
  • AddGroupLink:调用TaskService的addGroupIdentityLink方法时设置该值。
  • DeleteGroupLink:调用TaskService的deleteGroupIdentityLink方法时设置该值。
  • AddComment:调用TaskService的addComment方法时设置该值。
  • AddAttachment::调用TaskService的createAttachment方法时设置该值。
  • DeleteAttachment:调用TaskService的deleteAttachment方法时设置该值。

根据产生的数据结果,可以得出结论,除了addComment方法外,其他的方法产生的数据中,TYPE字段值均为“event’”,表示这些数据为事件记录数据;而addComment方法产生的数据中,TYPE字段值为“comment’”,表示这是一条任务(流程)的评论数据。

5.4、数据查询

TaskService中提供了以下几个查询评论表数据的方法。

  • getComment(String commentld):根据评论数据ID查询评论数据。
  • getTaskComments(String taskId):根据任务ID查询相应的评论数据。
  • getTaskEvents(String taskId):根据任务ID查询相应的事件记录。
  • getProcessInstanceComments(String processInstanceld):根据流程实例ID查询相应的评
    论(事件)数据。

其中,getTaskEvents方法返回Event的实例集合,而getTaskComments和getProcessInstanceComments方法返回Comment的实例集合。

ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
RuntimeService runtimeService = engine.getRuntimeService();
RepositoryService repositoryService = engine.getRepositoryService();
// 部署流程描述文件
Deployment dep = repositoryService.createDeployment().addClasspathResource("demo8/vacation.bpmn").deploy();
// 查找流程定义
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().deploymentId(dep.getId()).singleResult();
// 启动流程
ProcessInstance pi = runtimeService.startProcessInstanceById(pd.getId());
// 查找任务
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
// 调用各个记录事件的方法
taskService.addComment(task.getId(), pi.getId(), "this is comment message");
taskService.addUserIdentityLink(task.getId(), "1", "user");
taskService.deleteUserIdentityLink(task.getId(), "1", "user");
taskService.addGroupIdentityLink(task.getId(), "1", "group");
taskService.deleteGroupIdentityLink(task.getId(), "1", "group");
Attachment atta = taskService.createAttachment("test", task.getId(), pi.getId(), "test", "test", "");
taskService.deleteAttachment(atta.getId());
// 查询事件与评论
List<Comment> commonts1 = taskService.getProcessInstanceComments(pi.getId());
System.out.println("流程评论(事件)数量:" + commonts1.size());
commonts1 = taskService.getTaskComments(task.getId());
System.out.println("任务评论数量:" + commonts1.size());
List<Event> events = taskService.getTaskEvents(task.getId());
System.out.println("事件数量:" + events.size());
流程评论(事件)数量:3
任务评论数量:1
事件数量:7

6、任务声明与完成

任务声明实际上是指将任务分配到某个用户下,即将该用户作为该任务的代理人,可以使用TaskService的claim方法进行任务代理人指定(效果类似于setAssignee方法)。当一个任务需要完结时,可以调用TaskService的complete方法,指定该任务已经完成,让整个流程继续往下进行。

6.1、任务声明

调用Taskservice的claim方法可以将任务分配到用户下,即设置任务表的ASSIGNEE字段值为用户的ID,该效果与使用setAssignee方法的效果类似,但是不同的是,一旦调用了claim方法声明任务的代理人,如果再次调用该方法将同一个任务分配到另外的用户下,则会抛出异常。

ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
RuntimeService runtimeService = engine.getRuntimeService();
RepositoryService repositoryService = engine.getRepositoryService();
// 部署流程描述文件
Deployment dep = repositoryService.createDeployment().addClasspathResource("demo8/vacation.bpmn").deploy();
// 查找流程定义
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().deploymentId(dep.getId()).singleResult();
// 启动流程
ProcessInstance pi = runtimeService.startProcessInstanceById(pd.getId());
// 查找任务
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
// 调用claim方法
taskService.claim(task.getId(), "1");
// 此处将会抛出异常
taskService.claim(task.getId(), "2");
org.activiti.engine.ActivitiTaskAlreadyClaimedException: Task '35008' is already claimed by someone else.

6.2、任务完成

TaskService提供了多个完成任务的方法,对于第一个complete方法,只需要提供任务的ID即可,另外几个complete方法需要提供任务参数。当完成一个任务需要若干参数时,可以使用带参数的complete方法。例如现在有一个填写请假单的任务,完成这个任务时,需要提供请假天数等参数,那么可以使用带参数的complete方法,可以传入Map参数。假设现在有一个假期申请流程,需要由员工填写,经理审批,流程图如图所示。

在这里插入图片描述

<process id="vacationProcess" name="vacation">
  <startEvent id="startevent1" name="Start"></startEvent>
  <userTask id="usertask1" name="Write Vacation"></userTask>
  <userTask id="usertask2" name="Audit"></userTask>
  <sequenceFlow id="flow1" name="" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
  <sequenceFlow id="flow2" name="" sourceRef="usertask1" targetRef="usertask2"></sequenceFlow>
  <endEvent id="endevent1" name="End"></endEvent>
  <sequenceFlow id="flow3" name="" sourceRef="usertask2" targetRef="endevent1"></sequenceFlow>
</process>
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
RuntimeService runtimeService = engine.getRuntimeService();
RepositoryService repositoryService = engine.getRepositoryService();
// 部署流程描述文件
Deployment dep = repositoryService.createDeployment().addClasspathResource("demo8/vacation2.bpmn").deploy();
// 查找流程定义
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().deploymentId(dep.getId()).singleResult();
// 启动流程
ProcessInstance pi = runtimeService.startProcessInstanceById(pd.getId());
// 查找任务
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
// 调用complete方法完成任务,传入参数
Map<String, Object> vars = new HashMap<String, Object>();
vars.put("days", 2);
// 设置临时的参数
Map<String, Object> vars2 = new HashMap<String, Object>();
vars2.put("temp", "temp var");
taskService.complete(task.getId(), vars, vars2);
// 再次查找任务
task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
// 无法查询临时参数
String tempVar = (String)taskService.getVariable(task.getId(), "temp");
System.out.println("查询临时参数:" + tempVar);

//得到参数
Integer days = (Integer)taskService.getVariable(task.getId(), "days");
if (days > 5) {
    System.out.println("大于5天,不批");
} else {
    System.out.println("小于5天,完成任务,流程结束");
    taskService.complete(task.getId());
}

分别调用两个complete方法,在调用第一个complete方法
时,传入了一个名称为“days”的参数与一个名称为“temp”的参数,其中temp为临时参数。当流程进行到下一个任务时,判断days参数是否大于5,如果小于5,则完成流程。除了以上两个complete方法外,还有一个设置参数是否为Local的complete方法,其主要用于设置参数的作用域,在此不再赘述。

在调用complete方法时,会将完成的Task数据从任务表中删除,如果它发现这个任务为流程中的最后一个任务,则会连同流程实例的数据也一并删除,并且按照历史(history)配置来记录流程的历史数据。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值