springboot集成Camunda审核流程(三):Camunda审核流程常用API的测试示例

Springboot集成Camunda

三、Camunda-常用API简介

1、 流程定义模板部署接口

​ 部署流程所使用到的CamundaBean对象为 RepositoryService。具体封装操作如下

package cn.zhidasifang.camundaproject.camundaProcessFlow.Service.impl;

import cn.zhidasifang.camundaproject.camundaProcessFlow.Service.CamundaProcessDefService;
import cn.zhidasifang.camundaproject.utils.LogUtils;
import com.alibaba.fastjson.JSONObject;
import org.camunda.bpm.engine.RepositoryService;
import org.camunda.bpm.engine.repository.Deployment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @Description: 流程定义Service--实现类
 * @ClassName: ProcessDefinitionServiceImpl
 */
@Service
public class ProcessDefinitionServiceImpl implements CamundaProcessDefService {
    public  final Logger logger = LoggerFactory.getLogger(this.getClass());

    //此服务提供用于管理和操作部署和流程定义的操作,使用camunda的第一要务
    @Autowired
    RepositoryService repositoryService;

    
     /**
     * resource: 对应的是BPMN文件在resources资源文件下的路径!!!
    *@Description-【流程部署方式一】通过‘为部署指定给定名称’、‘流程资源文件路径全名称(放在resource下.bpmn)’--部署一个流程定义模板
    *@Param [name, resource]
    *@return com.alibaba.fastjson.JSONObject
    */
    @Override
    public Deployment deployDefinitionByResource(String name, String resource) {
        Deployment deploy = repositoryService.createDeployment()
                .name(name)
                .addClasspathResource(resource)
                .deploy();
        LogUtils.writeLogger(logger,"流程id="+deploy.getId()+"   流程Name="+deploy.getName()+"   流程Source="+deploy.getSource(),"新流程部署成功");
        return deploy;
    }

    
   /**
    *@Description-【流程部署方式二】通过‘为部署指定给定名称’、‘流程资源名称’、‘流程资源路径+资源名称’--部署一个流程定义模板
    *@Param [definitionName, resourceName, resource]
    *@return org.camunda.bpm.engine.repository.Deployment
    */
    @Override
    public Deployment deployDefinitionByString(String definitionName, String resourceName, String resource) {
        Deployment deploy = repositoryService.createDeployment()
                .name(definitionName)
                .addString(resourceName + ".bpmn", resource)
                .deploy();
        return deploy;
    }
}

推荐使用第一种方式:

.name( ) :该方法是指定流程部署时使用的名称,对应流程部署数据表 ACT_RE_DEPLOYMENT 中的流程部署名称NAME_

.addClasspathResource(resource) :对应写入流程文件在项目中的相对路径;eg:BPMN/xxx.bpmn。表示对项目中 resources 文件下的BPMN文件中 xxx.bpmn 流程进行部署操作。

在这里插入图片描述

​ 还需要区分的:在设计流程模板时,bpmn流程模板文件中的

ID: 在部署后就是我们流程的流程定义Key(PROC_DEF_KEY_),ID相同的BPMN流程文件,在部署后就会出现版本迭代的情况。

Name:name字段在流程部署之后就是对应的是我们流程模板的名称,在流程模板表 ACT_RE_PROCDEF 中均可查询到这些数据。

2、 开启流程实例接口

​ 启动一个新的流程实例,可以通过两种方式来启动:

  1. 通过流程模板定义key
  2. 通过流程模板定义id。两种方式的方法都包含多个重载@OverLoad。

​ 其中最重要的一个参数是 processDefinitionKey 流程模板定义key / processDefinitionId 流程模板定义id。启动流程实例需要用到的camundaBean对象为 RuntimeService。其次就是另一个服务对象 IdentityService ,通过该对象来完成用户相关的操作,实际中用不太到。

//通过流程模板定义key启动
ProcessInstance startProcessInstanceByKey(String processDefinitionKey, String businessKey, String caseInstanceId, Map<String, Object> variables);
//通过流程模板定义id启动
ProcessInstance startProcessInstanceById(String processDefinitionId, String businessKey, String caseInstanceId, Map<String, Object> variables);
//processdefinitionKey-业务密钥的组合必须是唯一的。参数:进程定义 – 进程定义的 id,不能为空。
//businessKey – 在给定流程定义的上下文中唯一标识流程实例的键。
//caseInstanceId – 用于将流程实例与案例实例相关联的案例实例的 ID。

	/**
    *@Description--【创建流程实例】--参数:‘流程定义key’、‘业务主键businessKey’、‘流程发起人initiator’
    *@Param [processDefinitionKey, BusinessKey, initiator]
    *@return org.camunda.bpm.engine.runtime.ProcessInstance
    */
    @Override
    public ProcessInstance startProcessInstanceByKey(String processDefinitionKey, String businessKey,String initiator) {
        // TODO: 2023-10-11  该处需要通过当前结点的审核人/发起人,进行判断获取到下一几点的审核人!!!!
        /*
        * mao中的key,对应设置流程时Assignee字段下所设置的表达式key 即:${userTwo}
        * 猜测可以一下设置下面所有结点的审核人,也可以只设置当前结点的下一个结点审核人
        * */
        HashMap<String, Object> map = new HashMap<>();
        map.put("initiator",initiator);
        map.put("userTwo","wuYong");
        map.put("userThree","songJiang");
        //设置流程发起人
        ProcessInstance instance = null;
        try {
            /*
            * 该操作的作用暂不清楚,但是他会改变 ACT_HI_PROCINST.START_USER_ID_字段的数据!
            * ---用来记录流程发起人的数据信息!!FlowAble中该字段是null,即没有调用该接口
            * */
            identityService.setAuthenticatedUserId(initiator);
            instance = runtimeService.startProcessInstanceByKey(processDefinitionKey, businessKey,map);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return instance;
        
        /*
         * 方式二
         * runtimeService.startProcessInstanceById();
         * */
    } 

​ 方法中的 identityService.setAuthenticatedUserId(initiator); 该方法的作用,我目前只是探测出来了一点就是,该方法传入的数据,会被存储到 历史流程实例表 ACT_HI_PROCINST 表中的 START_USER_ID_字段赋值。其他的作用暂时还未查询出来!

​ 在创建一个流程实例时我们需要注意:在即将运行到某一结点之前,我们必须先将该节点上设置的参数进行赋值(即在创建流程模板时,我设置的待赋值变量${xxxx})。我们可以提前将整个流程的参数进行赋值;或者是利用节点上的过滤器和监听器,实现动态的赋值。

3、 流程实例相关接口

3.1 删除流程实例

​ 首先要知道一点是,通过内置接口删除已经生成的流程实例时,所有的 ACT_HI_* 类型的历史数据表中的数据都是通过物理删除(数据库中原数据依然存在,会在状态字段中做删除标识),不会直接删除掉原有数据。

eg: ACT_HI_TASKINST.DELETE_REASON_ = deleted

在删除流程实例时,流程相关的结点数据也会被连带删除。

​ 调用流程删除接口的Bean对象是 runtimeService,通过流程实例的id:processInstance,然后调用接口进行删除,具体代码如下:

 @Override
    public void deletePROCINSTById(String processInstanceId){
        try {
            runtimeService.deleteProcessInstance(processInstanceId,"删除流程实例操作!");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
3.2 流程实例激活与挂起

​ 流程实例一旦被执行挂起操作之后,后续就不能在对该单据进行任务操作,只有流程激活后方可恢复流程中的所有操作!

//流程激活操作
runtimeService.activateProcessInstanceById(processInstanceId);

//流程挂起操作
runtimeService.suspendProcessInstanceById(processInstanceId);

​ 无论是执行激活还是挂起操作,数据库中对应流程的所有实例(流程实例,节点实例)所对应的版本号字段 REV_ 都会在原来的基础上+1。

​ 在执行流程挂起操作后,对应流程实例表 ACT_HI_PROCINST 中的 STATE_字段会变为暂停挂起的标识 SUSPENDEND

​ 同理在对流程实例进行激活时,会将流程实例数据表中对应流程的 STATE_字段改为 ACTIVE。正常运行时的流程实例该字段的状态也为ACTIVE

流程历史实例表 ACT_HI_PROCINST 表中所有单据的状态分别有:ACTIVE 正在运行中、COMPlETED 流程已审核结束、INTERNALLY_TERMINATED 流程已被内部终止结束。等三种情况。

3.3 重启流程实例

​ 重启流程实例是指,当流程运行结束时,重新启动该流程的审核,并使流程从指定的节点开始审核,不需要在从制单人处开始启动流程节点。

runtimeService.restartProcessInstances(processDefinitionId)
     .processInstanceIds(processInstanceId)
     //重新启动变动节点位置
     .startBeforeActivity(nodeId)
     //.startAfterActivity(taskId)
     //startTransition(String transitionId)
     .execute();

​ 其中的 .startBaforeActivity(nodeId) 就是指代从那一节点开始重新启动该流程,nodeId需要传入某一节点的 ACT_ID_ 。

3.4 修改流程实例运行位置

​ 在流程启动后,我们的流程正常情况下是按照流程定义模板进行流程节点的审核。但是很多时候在审核节点上审核不通过,就需要将该流程从新打会某一节点修改后在进行审核。这就需要我们用到就节点流程实例运行位置的修改。在实际场景中的驳回操作就是运用了这种方式。灵活地调整节点的审核位置。

//获取流程实例 
ActivityInstance activityInstance = runtimeService.getActivityInstance(processInstanceId); 
//获取流程实例修改器
ProcessInstanceModificationBuilder processInstanceModification = runtimeService.createProcessInstanceModification(processInstanceId);
//修改流程实例到指定的某一节点
ProcessInstanceModificationInstantiationBuilder processInstanceModificationInstantiationBuilder = processInstanceModification.startBeforeActivity(actId);
//为新节点添加variable零时参数
if (variable != null) processInstanceModificationInstantiationBuilder.setVariables(variable); 
//取消当前正在运行的流程节点 (不取消之前的审核节点就会致使该流程有两个待审核节点的出现)
processInstanceModificationInstantiationBuilder
                    .cancelAllForActivity(activityInstance.getId()) //取消正在运行的环节实例
                    .execute();

​ 在修改之前我们还应该做的一个操作是,完成当前节点:将当前节点结束掉taskService.createComment(task.getId(),task.getProcessInstanceId(),“commit-驳回操作!”);

​ 这几个接口只是临时操作可以这样,在实际的应用中时,我们还需要考虑当前节点的位置,驳回流程实例到哪一个审核环节,已经引入审核人后,还需做一系列的审核权限判断等操作。

4、 查询相关的接口

​ 在查询接口中需要注意我们返回数据的类型,当我们直接使用 Camunda 提供的 Task (import org.camunda.bpm.engine.task.Task;)作为查询的结果进行返回时,会出现返回结果错误问题,原因是Task接口 的实现类中,存在一些为null 的字段,当我们直接作为结果返回时,这些空属性会出现NullPotionterException异常,导致序列化错误。

4.1查询代办任务

​ 查询接口相对运用起来就比较简单,但是需要我们提前封装一个vo对象保存每一条任务节点的信息,vo对象中预设字段可以灵活的根据我们需求来确定节点中的那些数据是我们需要的。

package cn.zhidasifang.camundaproject.camundaProcessFlow.Service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.zhidasifang.camundaproject.camundaProcessFlow.Service.CamundaQueryService;
import cn.zhidasifang.camundaproject.camundaProcessFlow.entity.TaskVo;
import org.camunda.bpm.engine.HistoryService;
import org.camunda.bpm.engine.IdentityService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.TaskService;
import org.camunda.bpm.engine.task.Task;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;

/**
 * @Description: 流程相关数据查询接口
 * @ClassName: PROCQuerySeryServiceImpl
 */
@Service
public class PROCQueryServiceImpl implements CamundaQueryService {
    @Autowired
    private TaskService taskService;
    
    /**
    *@Description-【查询我的代办任务】-通过用户id--查询该用户id下的代办任务-(还可以进行分页查询!)
    *@Param [userId]
    *@return java.util.List<org.camunda.bpm.engine.task.Task>
    */
    @Override
    public List<TaskVo> queryMyTodoTask(String userId) {
        List<Task> tasks;
        try {
            tasks = taskService.createTaskQuery()
                    .taskAssignee(userId)
                    .listPage() 【可分页查询】
                    .list();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        /**
         *  Task只是个接口,直接返回会有序列化问题,但是可能和camunda版本也有关系!!!!!!!
         * */
        List<TaskVo> taskVos = null;
        /** hutool工具类判断集合是否为null */
        if (CollUtil.isNotEmpty(tasks)) {
            taskVos = new ArrayList<>();
            TaskVo taskVo= null;
            for (Task task : tasks) {
                /** BeanUtils组件可以实现对象属性的拷贝,copyProperty 实现对象属性的拷贝 */
                BeanUtils.copyProperties(task,taskVo=new TaskVo());
                taskVos.add(taskVo);
            }
        }
        return taskVos;
    }
}

其中的TaskVo是自定义的类型,目的是为了解决 Task直接返回时产生序列化的问题

package cn.zhidasifang.camundaproject.camundaProcessFlow.entity;
import lombok.Data;
import java.io.Serializable;
/**
 * @Description: 任务结点对象
 * @ClassName: TaskVo
 */
@Data
public class TaskVo implements Serializable {
    private String id;
    protected String name;
    private String processInstanceId;
}
4.2 查询待办任务节点
@Override
public ApiResult queryMyFinishTask(String userId, String businessKey) {
    List<HistoricTaskInstance> complete;
    try {
        complete = historyService.createHistoricTaskInstanceQuery()
            .taskAssignee(userId)
            //.processInstanceBusinessKey(businessKey)  暂时不添加业务标识代码
            .finished()
            .taskDeleteReason("completed") //过滤条件--这里表示已经执行完毕的任务结点
            .list();
    } catch (Exception e) {
        LogUtils.writeErrorException(logger, "查询我的已办失败", e);
        return ApiResult.fail("查询我的已办失败", e);
    }
    return ApiResult.success("查询我的已办成功!", CollUtil.isEmpty(complete) ? "null" : complete);
}
4.3 查询节点审核人List
List<Task> list =  taskService.createTaskQuery()
                    .processInstanceId(processInstanceId)
                    .taskId(taskId)
                    .list();
 List<String> assigneeList = list.stream().map(Task::getAssignee).collect(Collectors.toList());
4.4 流程定义信息的查询方式
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                    .processDefinitionId(processDefinitionId)
                    //.processDefinitionVersion(3) 指定版本号!
                    //.latestVersion() 最近的版本!
                    .singleResult();
4.5 审核日志的查询

​ 审核日志的查询:首先通过HistryService 查询出流程中已经执行结束的历史ActivityInstance 数据list

 List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(processInstanceId)
                .orderByHistoricActivityInstanceStartTime() //这里开始时间相同的可以,只能前端在根据结束时间继续排序
                .asc()
                .list();

​ 获取到历史activityInstance实例List后 可以经过遍历每一个历史任务节点数据。获取到对应节点中相关信息。审核节点中我们可以获取很多详细数据信息,比如:节点类型(userTask、sendTask)、节点名称、节点受理人assignee、节点审核意见、节点审核结束时间…等。需要根据实际业务需求封装查询出来的日志数据。

for (HistoricActivityInstance historicActivityInstance : list) {
            Map<String,Object> map=new HashMap<>();
            String taskId = historicActivityInstance.getTaskId();
            List<Comment> taskComments = taskService.getTaskComments(taskId);
            System.out.println("taskId = " + taskId);
            System.out.println(taskComments.size());
            map.put("activityName",historicActivityInstance.getActivityName());
            System.out.println("historicActivityInstance.getActivityType() = " + historicActivityInstance.getActivityType());
            map.put("activityType",matching(historicActivityInstance.getActivityType()));
            map.put("assignee",historicActivityInstance.getAssignee()==null?"无":historicActivityInstance.getAssignee());
            map.put("taskId",historicActivityInstance.getTaskId());

​ 其中 taskService.getTaskComments(taskId).就是获取到任务节点审核是,用户提交的Comment意见信息。数据对应数据表为 ACT_HI_COMMENT。

5、 任务审核通过接口

​ 目前暂时还没将审核人userId 纳入审核流程中,后续还会更新接口,通过审核人userId,首先判断该操作员是否对当前单据具有审核的权限!(目前的所有接口都是以单租户的情况下定义的)

  • 审核通过接口Controller层
package cn.zhidasifang.camundaproject.camundaProcessFlow.Controller;

import cn.zhidasifang.camundaproject.camundaProcessFlow.Service.CamundaTaskService;
import cn.zhidasifang.camundaproject.camundaProcessFlow.tools.ApiResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
 * @Description: 任务相关接口 Controller控制层
 * @ClassName: PROCTaskController
 */
@RestController
@RequestMapping("/processTask")
public class PROCTaskController {
    @Autowired
    CamundaTaskService camundaTaskService;

    @GetMapping("/taskComplete/{taskId}")
    @ResponseBody
    public ApiResult taskComplete(@PathVariable(name = "taskId")String taskId){
        System.out.println("taskId = " + taskId);
        return camundaTaskService.taskComplete(taskId);
    }
}
  • 审核通过接口Service层接口
package cn.zhidasifang.camundaproject.camundaProcessFlow.Service.impl;

import cn.zhidasifang.camundaproject.camundaProcessFlow.Service.CamundaTaskService;
import cn.zhidasifang.camundaproject.camundaProcessFlow.tools.ApiResult;
import cn.zhidasifang.camundaproject.utils.LogUtils;
import org.camunda.bpm.engine.TaskService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
 * @Description: 流程任务结点相关接口
 * @ClassName: PROCETaskServiceImpl
 */

@Service
public class PROCTaskServiceImpl implements CamundaTaskService {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    TaskService taskService;

    @Override
    public ApiResult taskComplete(String taskId) {
        try {
            taskService.complete(taskId);
        } catch (Exception e) {
            LogUtils.writeException(logger,"任务审核失败!",e);
            return ApiResult.fail("任务审核失败!",e);
        }
        return ApiResult.success("任务审核通过!");
    }
}
  • 任务审核 Version2 审核权限的判断!

    ​ 这里通过审核人 assignee 以及任务节点id进行了一个节点的查询。通过该查询方式未能查询到对应的节点信息的情况下,要不就是节点id错误,或者该节点目前不是该审核人审核,两种情况都说明了该节点没有审核权限。以这种方式简单的做了一个权限的判断!

 @Override
    public ApiResult taskComplete(String taskId,String assignee) {
        /**
         * 先通过 taskId、assignee 判断用户节点是否存在,存在才能审核(存在才具有权限)!!
         * */
        Task task = null;
        try {
            task = taskService.createTaskQuery()
                    .taskId(taskId)
                    .taskAssignee(assignee)
                    .singleResult();
        } catch (Exception e) {
            LogUtils.writeException(logger, "任务审核失败!", e);
            return ApiResult.fail("任务审核失败!", e.getMessage());
        }
        if (ObjectUtil.isNull(task)){
            LogUtils.writeLogger(logger, "没有查询到相关任务,或当前审核人没有权限!","审核任务失败!");
            return ApiResult.fail("任务审核失败:为查询到相关任务或当前审核人没有权限!");
        }else {
            /*
            * 审核意见的提交
            * */
            Comment comment = taskService.createComment(taskId, task.getProcessInstanceId(), "审核通过!!test");
            System.out.println("comment.toString() = " + comment.toString());
            taskService.complete(taskId);
        }
        return ApiResult.success("任务审核成功!");
    }

​ 如果通过审核人assignee 和 taskId节点Id 查询出来了对应的单据,说明当前的操作员是具有审核权限的,下面就继续进行审核相关的操作。

​ taskService.createComment(taskId,processInstanceid,审核意见):这里是给指定的目标节点添加以意见数据。在下一步我们执行complete完结任务节点后,我们对应添加的审核意见数据就会持久化到 ACT_HI_ACT_HI_COMMENT 表中对应节点任务数据上。

6、 其他类型的接口整合

6.1 任务Delegate委托接口

​ 委托任务就是指,将当前节点任务的审核权限委托给另一个人,其实也就是修改指定审核节点的受理人assignee,通过调用Delegate委托接口之后,我们可以通过数据表中查看到持久化数据的变化:

​ 在ACT_RU_TASK 或者 ACT_HI_TASKINST ,数据表中的 ASSIGNEE_字段 和 OWNER_字段 。 assignee字段表示的是当前任务节点的审核人,在我们调用Delegate委托接口之后,该assignee字段的审核人就会变为委托的人,那么节点之前的审核人就会存入OWNER_字段中。

@Override
public ApiResult taskDelegateOther(String taskId, String userId) {
    try {
        taskService.delegateTask(taskId,userId);
    } catch (Exception e) {
        LogUtils.writeErrorException(logger,"任务委派失败!",e);
        return ApiResult.fail("任务委派失败!",e);
    }
    /* 方式二:
         * 这里建议使用第二种,第一种按照官网的意思被委托人必须使用resolveTask(String)向任务所有者报告,这里存在一些权限的问题,当然我在使用过程中没加那些权限,所以可以正常使用。
         * taskService.setAssignee(taskId,userId);
         * */
    return ApiResult.success("委派成功!");
}
6.2 流程实例位置修改

​ 在时间应用场景中的驳回就是直接操作流程中节点的位置,重新创建流程中节点的运行位置,然后同时取消掉原始的正在运行时位置上的节点。

//任务流程创建了提交模板Comment 但是没有提交 taskService.complete(taskId)。--所有节点也不会提交到后台去
taskService.createComment(task.getId(),task.getProcessInstanceId(),"驳回原因:"+comment);
//任务流程实例修改位置
runtimeService.createProcessInstanceModification(task.getProcessInstanceId())
    //.cancelActivityInstance(ACT_ID_) //关闭当前节点相关的任务
    .cancelAllForActivity(task.getTaskDefinitionKey())
    .setAnnotation("进行了驳回到第一任务节点操作!")
    .startBeforeActivity(toActId) //启动目标活动节点
    .setVariables(taskVariable)
    .execute();

​ 在对任务节运行位置进行修改后,我们需要取消掉之前存在的待审核节点,否则会出现同一个流程实例中,存在着两个运行中的节点任务,取消之前的审核节点有两种方式:cancelActivityInstance(actId):关闭指定的一个任务节点, cancelAllForActivity(actId) 关闭所有的指定actId任务节点(这里主要是在会签节点中,会产生多个任务节点,但是这些所有的任务节点的actId 是一样的)所有会签节点中我们需要采用这种方式取消对应的审核节点中的所有任务。

​ **注:**这章节就简单的模拟了一些Camunda中内置API的使用,后续会将这些接口整合到实际应用中,结合单据业务标识 BusinessKey 以及动态设置受理人 来使用,主要是设计到了如下几种接口的封装:启动流程实例、单据的送审、单据的审核、单据的驳回操作、审核日志查询的封装、待审核列表查询、已审核列表的查询…等接口。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值