Flowable入门讲解

Flowable入门讲解

一、springboot项目整合Flowable6.4.2

1、导入相关做标:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.monkey_wang</groupId>
    <artifactId>monkey_wang</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <flowable.version>6.4.2</flowable.version>
    </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
    </parent>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <!--<scope>runtime</scope>-->
        </dependency>
        <!--web开发的起步依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 工作流 -->
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-spring-boot-starter</artifactId>
            <version>${flowable.version}</version>
        </dependency>
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-json-converter</artifactId>
            <version>${flowable.version}</version>
        </dependency>
    </dependencies>

</project>

2、配置文件中加入一下属性:

server.port=89
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.113.35:3306/flow
spring.datasource.username=root
spring.datasource.password=root
#关闭定时任务JOB
flowable.async-executor-activate=false
#将databaseSchemaUpdate设置为true。当Flowable发现库与数据库表结构不一致时,会自动将数据库表结构升级至新版本。
flowable.database-schema-update=true

二、部署定义好的流程

(1)、如果你的流程设计器和你当前的项目连的是同一个数据库的话:
controller层:

    /**
     * 部署流程
     */
    @GetMapping(value = "/deployByKey")
    @ApiOperationSupport(order = 4)
    @ApiOperation(value = "部署流程", notes = "部署流程")
    public R<String> deployByKey(@ApiParam(value = "流程定义的key", required = true) @RequestParam String processDefinitionKey) {
        try {
            processService.deployByKey(processDefinitionKey);
            return R.data("部署成功。");
        } catch (Exception e) {
            e.printStackTrace();
            return R.fail("服务器异常!");
        }
    }

service层:

    //需要注入的Flowable相关service:
    private final ModelRepository modelRepository;
    private final RepositoryService repositoryService;


    @Override
   public void deployByKey(String processDefinitionKey) {
       List<Model> models = modelRepository.findByKeyAndType(processDefinitionKey, 0);
       if (models.size() > 0) {
           for (Model model : models) {
               String id = model.getId();
               this.deployModel(id, "flow_2", null);
           }
       }
   }

   public boolean deployModel(String modelId, String category, List<String> tenantIdList) {
       FlowModel model = this.getById(modelId);
       if (model == null) {
           throw new ServiceException("No model found with the given id: " + modelId);
       }
       byte[] bytes = getBpmnXml(model);
       String processName = model.getName();
       if (!StringUtil.endsWithIgnoreCase(processName, FlowEngineConstant.SUFFIX)) {
           processName += FlowEngineConstant.SUFFIX;
       }
       String finalProcessName = processName;
       if (Func.isNotEmpty(tenantIdList)) {
           tenantIdList.forEach(tenantId -> {
               Deployment deployment = repositoryService.createDeployment().addBytes(finalProcessName, bytes).name(model.getName()).key(model.getModelKey()).tenantId(tenantId).deploy();
               deploy(deployment, category);
           });
       } else {
           Deployment deployment = repositoryService.createDeployment().addBytes(finalProcessName, bytes).name(model.getName()).key(model.getModelKey()).deploy();
           deploy(deployment, category);
       }
       return true;
   }

(2)、如果你的流程设计器和项目连的不是一个库:
首先你需要把你的流程图下载下来保存到本地,然后利用代码找到这个bpmn文件进行部署。

service层代码:

     /**
     * 部署流程
     * filePath 文件路径 name 流程名字
     */
    public Map<String, Object> deploymentFlow(String filePath, String name) {
        try {
            DeploymentBuilder deploymentBuilder = repositoryService.createDeployment()
                    .addClasspathResource(filePath).name(name);
            Deployment deployment = deploymentBuilder.deploy();
            logger.info("成功:部署工作流程:" + filePath);
            //acr_re_deployment表的id
            String id = deployment.getId();
            ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery();
            //搜索条件deploymentId
            query.deploymentId(id);
            //最新版本过滤
            query.latestVersion();
            //查询
            ProcessDefinition definition = query.singleResult();
            //act_re_procdef表的key和id
            String key = definition.getKey();
            String definitionId = definition.getId();
            Map<String, Object> map = new HashMap<>();
            map.put("流程定义的key", key);
            map.put("流程定义的的id", definitionId);
            return map;
        } catch (Exception e) {
            logger.error("失败:部署工作流:" + e);
            return null;
        }
    }

部署完成后,数据会保存在act_re_procdef和act_re_deployment表中

三、启动流程

注意: 后边讲解中会频繁调用RuntimeService、TaskService、HistoryService;这仨都是Flowable包下的服务类

  • RuntimeService :用于查询运行中的流程实例相关信息。
  • TaskService:用于查询代办、代签、签收、办理等操作。
  • HistoryService:顾名思义,查询流程相关历史数据。
    @PostMapping("/start")
    @ApiOperationSupport(order = 1)
    @ApiOperation(value = "启动流程", notes = "启动流程")
    @Transactional(rollbackFor = Exception.class)
    public R start(@ApiParam(value = "key", required = true) @RequestParam String key) {
        try {
            //构建流程启动需要的参数信息
            Map<String, Object> param = new HashMap<>();
            //param内封装的参数根据流程定义时的来
            //举个简单的栗子:比如流程线上定义了不同请假天数走不同的人审批;
            //请假天数参数名为day


            Integer day = 1;
            param.put("day", day);

            // 设置流程启动用户
            identityService.setAuthenticatedUserId(AuthUtil.getUserId().toString());
            ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(key, param);
            String processInstanceId = processInstance.getProcessInstanceId();
            return R.data(processInstanceId);
        } catch (Exception e) {
            e.printStackTrace();
            return R.fail("启动失败!");
        }
    }

启动完成之后,运行中的流程数据会保存到act_ru_actinst、act_ru_execution、act_ru_identitylink、act_ru_task

  • act_ru_actinst:存储运行中各个节点的信息:

  • act_ru_identitylink: 存储候选用户、处理人相关数据

  • act_ru_task :存储审批代办相关信息

四、查询审批代办

    @GetMapping("/getTask")
    @ApiOperationSupport(order = 2)
    @ApiOperation(value = "获取代办", notes = "获取代办")
    @Transactional(rollbackFor = Exception.class)
    public R getTask(@ApiParam(value = "processInstanceId", required = true) @RequestParam String processInstanceId) {
        //获取当前登录用户ID
        Long userId = AuthUtil.getUserId();
        //根据流程实例id和当前登录用户的id查询审批代办
        //注意: taskCandidateOrAssigned 表示不管是任务的候选人还是直接分配的人都可以查到
        //生产环境中可以不用结合流程实例查询;具体怎么查要结合具体业务来定。
        List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstanceId).taskCandidateOrAssigned(userId.toString()).list();
        if (!tasks.isEmpty()) {
            List<Map<String, Object>> list = new ArrayList<>();
            for (Task task : tasks) {
                Map<String, Object> map = new HashMap<>();
                //代办id
                String taskId = task.getId();
                //代办节点名称
                String taskName = task.getName();
                //创建时间
                Date createTime = task.getCreateTime();
                map.put("taskId", taskId);
                map.put("taskName", taskName);
                map.put("createTime", createTime);
                list.add(map);
            }
            return R.data(list);
        }
        return R.data("当前用户在当前流程实例中并无待办!");
    }

五、审批并跳转流程:

    @GetMapping("/audit")
    @ApiOperationSupport(order = 2)
    @ApiOperation(value = "审批并跳转流程", notes = "审批并跳转流程")
    @Transactional(rollbackFor = Exception.class)
    public R audit(@ApiParam(value = "taskId", required = true) @RequestParam String taskId) {
        try {
            //构建参数
            Map<String, Object> param = new HashMap<>();
            /*
            这个参数和启动流程时讲的参数是一个用法,这里不做过多的描述
            参数详情可以在表act_hi_varinst看到
            需要注意的是,它们都是全局的参数;可以通过
            historyService.createHistoricVariableInstanceQuery().processInstanceId(processInstanceId).list()或
            processInstance.getProcessVariables()获取
            */
            //跳转流程
            taskService.complete(taskId, param);
            return R.data("已审批");
        } catch (Exception e) {
            e.printStackTrace();
            return R.fail("跳转失败!");
        }
    }

ps : 流程跳转成功只后或流程结束后;对应的act_ru_task、act_ru_actinst等act_ru_打头的表数据会被删除。想要获取流程相关历史数据可以到act_hi_打头的表中查询,其结构和act_ru__打头的表结构大差不差甚至可以说相同;这样做主要是为了查询审批代办等运行中的流程数据时尽可能快。

六、转办

1、如果是候选人(candidaUser)发起的转办:

	@Override
	@Transactional(rollbackFor = Exception.class)
	public void turnTaskToOthers(String taskId, String userId) {
		Task task = taskService.createTaskQuery(taskId).taskId().singleResult();
		if (task == null) {
			return;
		}
		BladeUser user = AuthUtil.getUser();
		String userNickName = user.getNickName();
		User user1 = userMapper.selectById(Long.parseLong(userId));
		String realName = user1.getRealName();
		String comment = userNickName + "把该审批转办给了" + realName;
		//生成历史记录
		TaskEntity task1 = (TaskEntity) taskService.newTask(UUID.randomUUID().toString());
		task1.setParentTaskId(task.getParentTaskId());
		task1.setName(task.getName());
		task1.setAssignee(user.getUserId().toString());
		task1.setProcessInstanceId(task.getProcessInstanceId());
		task1.setProcessDefinitionId(task.getProcessDefinitionId());
		task1.setCategory(task.getCategory());
		task1.setTaskDefinitionKey(task.getTaskDefinitionKey());
		task1.setTaskDefinitionId(task.getTaskDefinitionId());
		taskService.saveTask(task1);
		taskService.addComment(task1.getId(), processInstanceId, comment);
		taskService.complete(task1.getId());
		//转办
		String taskId = task.getId();
		Boolean isAdmin = this.booleanAdmin();
		if (!isAdmin) {
			taskService.deleteCandidateUser(taskId, AuthUtil.getUserId().toString());
		}
		taskService.addCandidateUser(taskId, userId);
	}

2、如果是签收人(assign)发起的代办:

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void turnTaskToOthers(String taskId, String userId) {
        //获取源task
        Task task = taskService.createTaskQuery(taskId).taskId().singleResult();
        if (task == null) {
            return;
        }
        //获取流程名称
        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(tas.getProcessInstanceId).singleResult();
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey(processInstance.getProcessDefinitionKey()).latestVersion().singleResult();
        //获取任务接受用户账号用于发送通知
        User user = userService.getById(Long.parseLong(userId));
        String userAccount = user.getAccount();
        //获取任务源用户姓名
        BladeUser firstUser = AuthUtil.getUser();
        String userNickName = firstUser.getNickName();
        //获取任务名称
        String comment = userNickName + "把该审批转办给了" + user.getRealName();
        //生成历史记录
        TaskEntity task1 = (TaskEntity) taskService.newTask(UUID.randomUUID().toString());
        task1.setParentTaskId(task.getParentTaskId());
        task1.setName(task.getName());
        task1.setAssignee(task.getAssignee());
        task1.setProcessInstanceId(task.getProcessInstanceId());
        task1.setProcessDefinitionId(task.getProcessDefinitionId());
        task1.setCategory(task.getCategory());
        task1.setTaskDefinitionKey(task.getTaskDefinitionKey());
        task1.setTaskDefinitionId(task.getTaskDefinitionId());
        taskService.saveTask(task1);
        taskService.addComment(task1.getId(), processInstanceId, comment);
        taskService.complete(task1.getId());
        //转办
        taskService.unclaim(taskId);
        taskService.claim(taskId, userId);
    }

七、强制终止流程(作废)

    @Override
    public void stopProcessInstanceById(String processInstanceId) {
        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
        if (processInstance != null) {
            //1、获取终止节点
            List<EndEvent> endNodes = findEndFlowElement(processInstance.getProcessDefinitionId());
            String endId = endNodes.get(0).getId();
            List<Execution> executions = runtimeService.createExecutionQuery().parentId(processInstanceId).list();
            List<String> executionIds = new ArrayList<>();
            executions.forEach(execution -> executionIds.add(execution.getId()));
            runtimeService.createChangeActivityStateBuilder().moveExecutionsToSingleActivityId(executionIds, endId).changeState();
        }
    }

八、删除流程数据

    @Override
    public boolean deleteProcessInstance(String processInstanceId, String deleteReason) {
        //判断流程是否结束
        boolean finished = isFinished(processInstanceId);
        //删除运行中的流程实例
        if (!finished) {
            runtimeService.deleteProcessInstance(processInstanceId, deleteReason);
        }
        //删除历史实例
        historyService.deleteHistoricProcessInstance(processInstanceId);
        return true;
    }

    private boolean isFinished(String processInstanceId) {
        return historyService.createHistoricProcessInstanceQuery().finished()
                .processInstanceId(processInstanceId).count() > 0;
    }

九、流程跳转信息和节点进程图

流程跳转信息:

	@Override
	public List<BladeFlow> historyFlowList(String processInstanceId, String startActivityId, String endActivityId) {
		List<BladeFlow> flowList = new LinkedList<>();
		List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().orderByHistoricActivityInstanceEndTime().asc().list();
		boolean start = false;
		Map<String, Integer> activityMap = new HashMap<>(16);
		for (int i = 0; i < historicActivityInstanceList.size(); i++) {
			HistoricActivityInstance historicActivityInstance = historicActivityInstanceList.get(i);
			if (historicActivityInstance.getActivityName() == null) {
				continue;
			}
			// 过滤开始节点前的节点
			if (StringUtil.isNotBlank(startActivityId) && startActivityId.equals(historicActivityInstance.getActivityId())) {
				start = true;
			}
			if (StringUtil.isNotBlank(startActivityId) && !start) {
				continue;
			}
			// 显示开始节点和结束节点,并且执行人不为空的任务
			if ("userTask".equals(historicActivityInstance.getActivityType())
				|| FlowEngineConstant.START_EVENT.equals(historicActivityInstance.getActivityType())
				|| FlowEngineConstant.END_EVENT.equals(historicActivityInstance.getActivityType())) {
				// 给节点增加序号
				Integer activityNum = activityMap.get(historicActivityInstance.getActivityId());
				if (activityNum == null) {
					activityMap.put(historicActivityInstance.getActivityId(), activityMap.size());
				}
				BladeFlow flow = new BladeFlow();
				flow.setHistoryActivityId(historicActivityInstance.getActivityId());
				flow.setHistoryActivityName(historicActivityInstance.getActivityName());
				flow.setCreateTime(historicActivityInstance.getStartTime());
				Date endTime = historicActivityInstance.getEndTime();
				if (endTime != null) {
					flow.setFlag("ok");
					flow.setPass(true);
				}
				flow.setEndTime(endTime);
				String durationTime = DateUtil.secondToTime(Func.toLong(historicActivityInstance.getDurationInMillis(), 0L) / 1000);
				flow.setHistoryActivityDurationTime(durationTime);
				// 获取流程发起人名称
				this.getStartUser(historicActivityInstance, flow, processInstanceId);
				// 获取任务执行人名称
				if (StringUtil.isNotBlank(historicActivityInstance.getAssignee())) {
					this.getTaskAssign(historicActivityInstance, flow);
				} else {
					this.setCandidateUsers(historicActivityInstance, flow);
				}
				if (!StringUtil.isEmpty(flow.getTaskId()) && StringUtil.isEmpty(flow.getAssigneeName())) {
					continue;
				}
				// 获取意见评论内容
				if (StringUtil.isNotBlank(historicActivityInstance.getTaskId())) {
					List<Comment> commentList = taskService.getTaskComments(historicActivityInstance.getTaskId());
					if (commentList.size() > 0) {
						flow.setComment(commentList.get(0).getFullMessage());
					}
				}
				flowList.add(flow);
			}
			// 过滤结束节点后的节点
			if (StringUtils.isNotBlank(endActivityId) && endActivityId.equals(historicActivityInstance.getActivityId())) {
				boolean temp = false;
				Integer activityNum = activityMap.get(historicActivityInstance.getActivityId());
				// 该活动节点,后续节点是否在结束节点之前,在后续节点中是否存在
				for (int j = i + 1; j < historicActivityInstanceList.size(); j++) {
					HistoricActivityInstance hi = historicActivityInstanceList.get(j);
					Integer activityNumA = activityMap.get(hi.getActivityId());
					boolean numberTemp = activityNumA != null && activityNumA < activityNum;
					boolean equalsTemp = StringUtils.equals(hi.getActivityId(), historicActivityInstance.getActivityId());
					if (numberTemp || equalsTemp) {
						temp = true;
					}
				}
				if (!temp) {
					break;
				}
			}
		}
		return flowList;
	}

	private void getStartUser(HistoricActivityInstance historicActivityInstance, BladeFlow flow, String processInstanceId) {
		if (FlowEngineConstant.START_EVENT.equals(historicActivityInstance.getActivityType())) {
			List<HistoricProcessInstance> processInstanceList = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).orderByProcessInstanceStartTime().asc().list();
			if (processInstanceList.size() > 0) {
				if (StringUtil.isNotBlank(processInstanceList.get(0).getStartUserId())) {
					String taskUser = processInstanceList.get(0).getStartUserId();
					User user = UserCache.getUser(TaskUtil.getUserId(taskUser));
					if (user != null) {
						flow.setAssignee(historicActivityInstance.getAssignee());
						flow.setAssigneeName(user.getName());
					}
				}
			}
		}

	}

	private void setCandidateUsers(HistoricActivityInstance historicActivityInstance, BladeFlow flow) {
		String taskId = historicActivityInstance.getTaskId();
		if (StringUtils.isNotEmpty(taskId)) {
			flow.setTaskId(taskId);
			List<Task> list = taskService.createTaskQuery().taskId(taskId).list();
			if (list.isEmpty()) {
				return;
			}
			List<IdentityLink> identityLinksForTask = taskService.getIdentityLinksForTask(taskId);
			if (!identityLinksForTask.isEmpty()) {
				List<String> names = new ArrayList<>();
				for (IdentityLink identityLink : identityLinksForTask) {
					String type = identityLink.getType();
					String userId = identityLink.getUserId();
					if (StringUtils.isNotEmpty(type) && "candidate".equals(type) && StringUtils.isNotEmpty(userId)) {
						User user = userMapper.selectById(Long.parseLong(userId));
						if (user != null) {
							String realName = user.getRealName();
							if (StringUtils.isNotEmpty(realName)) {
								names.add(realName);
							}
						}
					}
				}
				if (!names.isEmpty()) {
					String substring = names.toString().substring(1, names.toString().length() - 1);
					flow.setAssigneeName(substring);
				}
			}
		}
	}

节点进程图:

    /**
     * 根据流程节点绘图
     *
     * @param processInstanceId   流程实例id
     * @param httpServletResponse http响应
     */
    private void diagram(String processInstanceId, HttpServletResponse httpServletResponse) {
        // 获得当前活动的节点
        String processDefinitionId;
        // 如果流程已经结束,则得到结束节点
        if (this.isFinished(processInstanceId)) {
            HistoricProcessInstance pi = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
            processDefinitionId = pi.getProcessDefinitionId();
        } else {
            // 如果流程没有结束,则取当前活动节点
            // 根据流程实例ID获得当前处于活动状态的ActivityId合集
            ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
            processDefinitionId = pi.getProcessDefinitionId();
        }
        List<String> highLightedActivities = new ArrayList<>();
        List<String> flows = new ArrayList<>();
        // 获得活动的节点
        List<HistoricActivityInstance> highLightedActivityList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().desc().list();
        if (!highLightedActivityList.isEmpty()) {
            for (HistoricActivityInstance activityInstance : highLightedActivityList) {
                highLightedActivities.add(activityInstance.getActivityId());
                if ("sequenceFlow".equals(activityInstance.getActivityType())) {
                    flows.add(activityInstance.getActivityId());
                }

            }
        }

        // 获取流程图
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
        ProcessEngineConfiguration engConf = processEngine.getProcessEngineConfiguration();
        ProcessDiagramGenerator diagramGenerator = engConf.getProcessDiagramGenerator();
        InputStream in = diagramGenerator.generateDiagram(bpmnModel, "bmp", highLightedActivities, flows, engConf.getActivityFontName(),
                engConf.getLabelFontName(), engConf.getAnnotationFontName(), engConf.getClassLoader(), 1.0, true);
        OutputStream out = null;
        byte[] buf = new byte[1024];
        int length;
        try {
            out = httpServletResponse.getOutputStream();
            while ((length = in.read(buf)) != -1) {
                out.write(buf, 0, length);
            }
        } catch (IOException e) {
            log.error("操作异常", e);
        } finally {
            IoUtil.closeSilently(out);
            IoUtil.closeSilently(in);
        }
    }

    /**
     * 是否已完结
     *
     * @param processInstanceId 流程实例id
     * @return bool
     */
    private boolean isFinished(String processInstanceId) {
        return historyService.createHistoricProcessInstanceQuery().finished()
                .processInstanceId(processInstanceId).count() > 0;
    }

效果图:

在这里插入图片描述

十、流程节点回退

类似于回滚,只能回退到用户任务节点;

举个简单的栗子:

流程:开始→A审批→B审批→C审批→结束

如果现在到B审批了,则只能回退到A审批;如果现在到了C审批,则可以回退到A审批或B审批

注:回退成功后,会生成新的代办(类似于事务回滚)。

@Override
	public void rollBackProcess(String proInstanceId, List<String> currTaskKeys, String targetKey) {
		runtimeService.createChangeActivityStateBuilder()
			.processInstanceId(proInstanceId)
			.moveActivityIdsToSingleActivityId(currTaskKeys, targetKey)
			.changeState();
	}
  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值