(2)SpringBoot 3 + Vue 3 前后端分离项目,集成 Flowable

(1)认识 Flowable 流程引擎

(2)SpringBoot 3 + Vue 3 前后端分离项目,集成 Flowable(正在浏览)

1. pom.xml 添加 Maven 依赖

        <!-- Flowable 流程引擎 -->
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-spring-boot-starter</artifactId>
            <version>7.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- mysql 连接驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>${mysql-connector-j.version}</version>
        </dependency>

        <!-- spring-boot3 , mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- mybatis-plus 分页插件,确保版本和 MyBatis Plus 主包一致 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-jsqlparser</artifactId>
            <version>${mybatis-plus-jsqlparser.version}</version>
        </dependency>

        <!-- mybatis-plus-join 多表查询 -->
        <dependency>
            <groupId>com.github.yulichang</groupId>
            <artifactId>mybatis-plus-join-boot-starter</artifactId>
            <version>${mybatis-plus-join.version}</version>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

2. application.yml 配置 Flowable,项目启动会自动在数据库创建 Flowable 需要的表

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/springboot3_vue3_satoken
    username: ***
    password: ***
    driver-class-name: com.mysql.cj.jdbc.Driver

flowable:
  # 是否开启异步执行器
  async-executor-activate: false
  # 建议初始化设置 true,生成所需要的表;之后设置为 false,则不会自动检查和更新数据库
  database-schema-update: false

3. 创建用户、组,并绑定用户-组的关系

3.1 根据 act_id_user、act_id_group、act_id_membership 表,分别创建 Entity、Controller、Service、ServiceImp、Mapper 。

3.2 Flowable 用户组 API

package com.dragon.springboot3vue3.controller.flowable;

import cn.dev33.satoken.util.SaResult;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dragon.springboot3vue3.controller.dto.commonDto.StringsDTO;
import com.dragon.springboot3vue3.controller.flowable.dto.pageDto.ActIdGroupPageDto;
import com.dragon.springboot3vue3.entity.flowable.ActIdGroup;
import com.dragon.springboot3vue3.service.flowableService.ActIdGroupService;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Tag(name = "Flowable 用户组 API")
@RestController
@RequestMapping("/actIdGroup")
public class ActIdGroupController {
    @Autowired
    private ActIdGroupService groupService;

    @Operation(summary = "分页列表")
    @PostMapping("/list")
    public SaResult list(@RequestBody ActIdGroupPageDto pageDto){
        // 创建分页对象
        Page<ActIdGroup> page = new Page<>(pageDto.getCurrentPage(), pageDto.getPageSize());

        // 构造多表查询条件
        MPJLambdaWrapper<ActIdGroup> qw = new MPJLambdaWrapper<ActIdGroup>()
                .like(StringUtils.isNotBlank(pageDto.getName()), ActIdGroup::getName, pageDto.getName());

        // 根据查询条件,将结果封装到分页对象
        Page<ActIdGroup> response = groupService.page(page, qw);

        return SaResult.ok().setData(response);
    }

    @Operation(summary = "新增或更新")
    @PostMapping("/saveOrUpdate")
    public SaResult saveOrUpdate(@RequestBody @Validated ActIdGroup actIdGroup){
        ActIdGroup group = new ActIdGroup();
        BeanUtils.copyProperties(actIdGroup, group);
        groupService.saveOrUpdate(group);
        return SaResult.ok();
    }

    @Operation(summary = "删除")
    @DeleteMapping("/remove")
    public SaResult remove(@RequestBody @Validated StringsDTO stringsDTO){
        groupService.removeByIds(stringsDTO.getStrings());
        return SaResult.ok();
    }

    @Operation(summary = "所有列表")
    @GetMapping("/getAll")
    public SaResult getAll(){
        return SaResult.ok().setData(groupService.list());
    }
}

3.3 Flowable 用户 API

package com.dragon.springboot3vue3.controller.flowable;

import cn.dev33.satoken.util.SaResult;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dragon.springboot3vue3.controller.dto.commonDto.StringsDTO;
import com.dragon.springboot3vue3.controller.flowable.dto.entityDto.ActIdUserDto;
import com.dragon.springboot3vue3.controller.flowable.dto.pageDto.ActIdUserPageDto;
import com.dragon.springboot3vue3.entity.flowable.ActIdGroup;
import com.dragon.springboot3vue3.entity.flowable.ActIdMembership;
import com.dragon.springboot3vue3.entity.flowable.ActIdUser;
import com.dragon.springboot3vue3.service.flowableService.ActIdMembershipService;
import com.dragon.springboot3vue3.service.flowableService.ActIdUserService;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Tag(name = "Flowable 用户 API")
@RestController
@RequestMapping("/actIdUser")
public class ActIdUserController {
    @Autowired
    private ActIdUserService actIdUserService;
    @Autowired
    private ActIdMembershipService actIdMembershipService;

    @Operation(summary = "分页列表")
    @PostMapping("/list")
    public SaResult list(@RequestBody ActIdUserPageDto pageDto){
        // 创建分页对象
        Page<ActIdUserDto> page = new Page<>(pageDto.getCurrentPage(), pageDto.getPageSize());

        // 构造多表查询条件
        MPJLambdaWrapper<ActIdUser> qw = new MPJLambdaWrapper<ActIdUser>()
                .selectAs(ActIdUser::getId, ActIdUserDto::getId)
                .selectAs(ActIdUser::getDisplayName, ActIdUserDto::getDisplayName)
                .selectAs(ActIdGroup::getId, ActIdUserDto::getGroupId)
                .selectAs(ActIdGroup::getName, ActIdUserDto::getGroupName)
                .leftJoin(ActIdMembership.class, ActIdMembership::getUserId, ActIdUser::getId)
                .leftJoin(ActIdGroup.class, ActIdGroup::getId, ActIdMembership::getGroupId)
                .like(StringUtils.isNotBlank(pageDto.getDisplayName()), ActIdUser::getDisplayName, pageDto.getDisplayName())
                .like(StringUtils.isNotBlank(pageDto.getGroupName()), ActIdGroup::getName, pageDto.getGroupName());

        // 根据查询条件,将结果封装到分页对象
        Page<ActIdUserDto> response = actIdUserService.selectJoinListPage(page, ActIdUserDto.class,qw);

        return SaResult.ok().setData(response);
    }

    @Transactional
    @Operation(summary = "新增或更新")
    @PostMapping("/saveOrUpdate")
    public SaResult saveOrUpdate(@RequestBody @Validated ActIdUserDto userDto){
        // 1.  保存用户
        ActIdUser user = new ActIdUser();
        BeanUtils.copyProperties(userDto, user);
        actIdUserService.saveOrUpdate(user);

        // 2. 保存用户-组关联关系
        ActIdMembership membership = new ActIdMembership();
        membership.setUserId(userDto.getId());
        membership.setGroupId(userDto.getGroupId());
        actIdMembershipService.saveOrUpdate(membership);

        return SaResult.ok();
    }

    @Operation(summary = "删除")
    @DeleteMapping("/remove")
    public SaResult remove(@RequestBody @Validated StringsDTO stringsDTO){
        actIdUserService.removeByIds(stringsDTO.getStrings());
        return SaResult.ok();
    }

    @Operation(summary = "查询所有用户列表")
    @GetMapping("/getAll")
    public SaResult getAll(){
        return SaResult.ok().setData(actIdUserService.list());
    }
}

4. 流程定义 API(首先部署流程定义)

@Slf4j
@Tag(name = "Flowable 流程引擎 API")
@RestController
@RequestMapping("/process")
public class ProcessController {
    @Autowired
    private TaskService taskService;
    @Autowired
    private RepositoryService repositoryService;
    @Autowired
    private RuntimeService runtimeService;
    @Autowired
    private IdentityService identityService;
    @Autowired
    private HistoryService historyService;
    @Autowired
    private ActReProcDefService actReProcDefService;
    @Autowired
    private ActIdMembershipService  actIdMembershipService;

    @Operation(summary = "上传文件部署流程定义")
    @PostMapping("/uploadFileDeploy")
    public SaResult uploadFileDeploy(@RequestParam("file") MultipartFile file) {
        try {
            repositoryService.createDeployment()
                    .addBytes(file.getOriginalFilename(), file.getBytes())
                    .name(file.getOriginalFilename())
                    .deploy();
            return SaResult.ok("流程部署成功 ");
        } catch (Exception e) {
            return SaResult.error("流程部署失败 ");
        }
    }

    @Operation(summary = "本地文件部署流程定义")
    @PostMapping("/localFileDeploy")
    public SaResult localFileDeploy() {
        // 本地文件部署,resources -> process -> leave-request.bpmn20.xml
        repositoryService.createDeployment()
                .addClasspathResource("process/leave-request.bpmn20.xml")
                .name("请假流程")
                .deploy();
        return SaResult.ok("部署成功");
    }

    @Operation(summary = "流程定义分页列表")
    @PostMapping("/list")
    public SaResult list(@RequestBody ActReProcDefPageDto pageDto) {
        // 创建分页对象
        Page<ActReProcDef> page = new Page<>(pageDto.getCurrentPage(), pageDto.getPageSize());

        // 构造多表查询条件
        MPJLambdaWrapper<ActReProcDef> qw = new MPJLambdaWrapper<ActReProcDef>()
                .like(StringUtils.isNotBlank(pageDto.getName()), ActReProcDef::getName, pageDto.getName());

        // 根据查询条件,将结果封装到分页对象
        Page<ActReProcDef> response = actReProcDefService.page(page, qw);

        return SaResult.ok().setData(response);
    }

    @Operation(summary = "流程定义删除")
    @DeleteMapping("/remove")
    public SaResult remove(@RequestBody StringDTO stringDTO) {
        // 根据 deploymentId 删除,是否级联删除(流程启动了也可以删除)
        repositoryService.deleteDeployment(stringDTO.getStr(), true);

        return SaResult.ok();
    }

    @Operation(summary = "挂起流程定义")
    @PostMapping("/suspend")
    public SaResult suspend(@RequestBody StringDTO stringDTO) {
        repositoryService.suspendProcessDefinitionByKey(stringDTO.getStr());
        return SaResult.ok("挂起流程定义");
    }

    @Operation(summary = "激活流程定义")
    @PostMapping("/activate")
    public SaResult activate(@RequestBody StringDTO stringDTO) {
        repositoryService.activateProcessDefinitionByKey(stringDTO.getStr());
        return SaResult.ok("激活流程定义");
    }
}

 5. 申请者开启流程实例(以请假流程为例)

    /**
     * 一个流程实例通常会包含多个任务节点,这些任务会在流程执行过程中逐步生成和分配
     */
    @Operation(summary = "开启请假流程实例")
    @PostMapping("/startLeave")
    public SaResult startLeave(@RequestBody HandleTaskDto handleTaskDto) {
        // 设置启动人
        identityService.setAuthenticatedUserId(handleTaskDto.getUserId());

        // 请假实例参数
        Map<String, Object> variables = new HashMap<>();
        variables.put("userId", handleTaskDto.getUserId());
        variables.put("groupId", handleTaskDto.getGroupId());

        // 启动流程实例,并设置流程变量
        runtimeService.startProcessInstanceByKey(handleTaskDto.getProcDefKey(), variables);

        return SaResult.ok();
    }

6. 申请者处理任务(填写请假信息)

    @Operation(summary = "用户处理任务")
    @PostMapping("/assigneeHandle")
    public SaResult assigneeHandle(@RequestBody HandleTaskDto handleTaskDto) {
        Task task = taskService.createTaskQuery()
                .taskAssignee(handleTaskDto.getUserId())
                .singleResult();

        // 请假实例参数
        Map<String, Object> variables = new HashMap<>();
        variables.put("userId", handleTaskDto.getUserId());
        variables.put("startTime", handleTaskDto.getStartTimeValue());
        variables.put("endTime", handleTaskDto.getEndTimeValue());
        variables.put("days", String.valueOf(handleTaskDto.getDays()));
        variables.put("reason", handleTaskDto.getReason());

        // 处理任务
        taskService.complete(task.getId(), variables);

        return SaResult.ok();
    }

7. 查看申请记录

    @Operation(summary = "查询流程实例历史记录列表")
    @PostMapping("/getProcessInstanceHistory")
    public SaResult getProcessInstanceHistory(@RequestBody ProcessInstanceHistoryPageDto pageDto) {
        // 创建分页对象
        Page<ActHiProcinstDto> page = new Page<>(pageDto.getCurrentPage(),pageDto.getPageSize());

        // 创建查询对象
        HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery()
                .orderByProcessInstanceStartTime().desc();

        // 根据启动人筛选流程实例
        if (StringUtils.isNotBlank(pageDto.getUserId())) {
            query = query.startedBy(pageDto.getUserId());
        }

        // 查询当前页的数据
        List<HistoricProcessInstance> historicTasks = query.listPage((int) ((pageDto.getCurrentPage() - 1) * pageDto.getPageSize()), Math.toIntExact(pageDto.getPageSize()));

        // 转换为 DTO 列表
        List<ActHiProcinstDto> list = historicTasks.stream()
                .map(task -> {
                    // 获取变量
                    Map<String, Object> variables = getApplyVariables(task.getId());
                    // 获取任务ID
                    HistoricTaskInstance managerApprovalTask = historyService.createHistoricTaskInstanceQuery()
                            .processInstanceId(task.getId())
                            .taskDefinitionKey("managerApproval")
                            .unfinished()
                            .singleResult();

                    return ActHiProcinstDto.builder()
                            .id(task.getId())
                            .name(task.getName())
                            .startTime(DateUtil.toLocalDateTime(task.getStartTime()))
                            .endTime(DateUtil.toLocalDateTime(task.getEndTime()))
                            .procDefId(task.getProcessDefinitionId())
                            .procDefName(task.getProcessDefinitionName())
                            .procDefKey(task.getProcessDefinitionKey())
                            .procInstId(task.getId())
                            .startUserId(task.getStartUserId())
                            .startTimeValue( (String) variables.get("startTime"))
                            .endTimeValue((String) variables.get("endTime"))
                            .days((String) variables.get("days"))
                            .reason((String) variables.get("reason"))
                            .userId((String) variables.get("userId"))
                            .result(variables.get("result") != null ? (Boolean) variables.get("result") : null)
                            .managerApprovalTaskId(managerApprovalTask!= null ? managerApprovalTask.getId() : null)
                            .build();
                        }
                ).toList();

        // 查询总记录数
        long total = query.count();

        // 封装分页结果
        page.setRecords(list);
        page.setTotal(total);

        return SaResult.ok().setData(page);
    }


    @Operation(summary = "查询历史记录中流程实例变量")
    private Map<String, Object> getApplyVariables(String processInstanceId) {
        HistoricVariableInstance days = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("days")
                .singleResult();
        HistoricVariableInstance startTime = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("startTime")
                .singleResult();
        HistoricVariableInstance endTime = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("endTime")
                .singleResult();
        HistoricVariableInstance userId = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("userId")
                .singleResult();
        HistoricVariableInstance reason = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("reason")
                .singleResult();
        HistoricVariableInstance result = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("result")
                .singleResult();

        HashMap<String, Object> map = new HashMap<>();
        map.put("days", days  != null ? days.getValue() : null);
        map.put("startTime", startTime  != null ? startTime.getValue() : null);
        map.put("endTime", endTime  != null ? endTime.getValue() : null);
        map.put("userId", userId  != null ? userId.getValue() : null);
        map.put("reason", reason != null ? reason.getValue() : null);
        map.put("result", result != null ? result.getValue() : null);
        return map;
    }

8. 查看流程进度

    @Operation(summary = "查询历史活动详情列表")
    @GetMapping("/getHistoryTask")
    public SaResult getHistoryTask(@RequestParam String processInstanceId) {
        List<HistoricActivityInstance> historicTasks = historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(processInstanceId)
                .orderByHistoricActivityInstanceEndTime().asc()
                .list();

        List<ActRuTaskDto> list = historicTasks.stream()
                .map(task -> ActRuTaskDto.builder()
                        .id(task.getActivityId())
                        .name(task.getActivityName())
                        .assignee(task.getAssignee())
                        .createTime(DateUtil.toLocalDateTime(task.getStartTime()))
                        .endTime(DateUtil.toLocalDateTime(task.getEndTime()))
                        .procInstId(task.getProcessInstanceId())
                        .procDefId(task.getProcessDefinitionId())
                        .build()
                ).toList();

        return SaResult.ok().setData(list);
    }

9. 审批者认领任务(认领候选组任务)

    @Operation(summary = "认领候选组任务")
    @PostMapping("/claimGroupTask")
    public SaResult claimGroupTask(@RequestBody HandleTaskDto handleTaskDto) {
        // 判断用户是否属于该用户组
        ActIdMembership one = actIdMembershipService.lambdaQuery()
                .eq(ActIdMembership::getUserId, handleTaskDto.getHandleUserId())
                .eq(ActIdMembership::getGroupId, handleTaskDto.getGroupId())
                .one();
        if(one == null){
            return SaResult.error("您无权限认领此任务");
        }
        // 认领任务
        taskService.claim(handleTaskDto.getManagerApprovalTaskId(), handleTaskDto.getHandleUserId());
        return SaResult.ok("任务已认领");
    }

10. 审批者处理任务(处理认领的任务)

    @Operation(summary = "认领用户处理认领的任务")
    @PostMapping("/handleClaimTask")
    public SaResult handleClaimTask(@RequestBody HandleTaskDto handleTaskDto) {
        Task task = taskService.createTaskQuery()
                .taskAssignee(handleTaskDto.getHandleUserId())
                .singleResult();

        if(task == null){
            return SaResult.error("任务已处理");
        }

        // 是否同意
        Map<String, Object> variables = new HashMap<>();
        variables.put("result", handleTaskDto.getResult());

        taskService.complete(task.getId(), variables);
        return SaResult.ok("任务处理完成");
    }

11. DTO

11.1 ActHiProcinstDto

package com.dragon.springboot3vue3.controller.flowable.dto.entityDto;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;

import java.time.LocalDateTime;

@Builder
@Data
public class ActHiProcinstDto {
    @Schema(description = "主键ID")
    private String id;

    @Schema(description = "修订版本号(用于乐观锁控制)")
    private Integer rev;

    @Schema(description = "业务键")
    private String businessKey;

    @Schema(description = "开始时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;

    @Schema(description = "结束时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;

    @Schema(description = "启动用户ID")
    private String startUserId;

    @Schema(description = "开始活动ID")
    private String startActId;

    @Schema(description = "结束活动ID")
    private String endActId;

    @Schema(description = "删除原因")
    private String deleteReason;

    @Schema(description = "租户ID")
    private String tenantId;

    @Schema(description = "流程实例名称")
    private String name;

    @Schema(description = "流程实例ID")
    private String procInstId;

    @Schema(description = "流程定义ID")
    private String procDefId;

    @Schema(description = "流程定义Key")
    private String procDefKey;

    @Schema(description = "流程定义名称")
    private String procDefName;

    @Schema(description = "用户ID")
    private String userId;

    @Schema(description = "开始时间")
    private String startTimeValue;

    @Schema(description = "结束时间")
    private String endTimeValue;

    @Schema(description = "请假天数")
    private String days;

    @Schema(description = "请假理由")
    private String reason;

    @Schema(description = "审批是否通过")
    private Boolean result;

    @Schema(description = "管理员审批的任务ID")
    private String managerApprovalTaskId;
}

11.2 ActIdUserDto

package com.dragon.springboot3vue3.controller.flowable.dto.entityDto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;

@Data
public class ActIdUserDto {

    @Schema(description = "用户ID")
    private String id;

    @Schema(description = "用户显示名称")
    private String displayName;

    @Schema(description = "邮箱")
    private String email;

    @Schema(description = "密码")
    private String pwd;

    @Schema(description = "租户ID")
    private String tenantId;

    @NotEmpty
    @Schema(description = "用户组ID")
    private String groupId;

    @Schema(description = "用户组名称")
    private String groupName;
}

11.3 ActRuTaskDto

package com.dragon.springboot3vue3.controller.flowable.dto.entityDto;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;

import java.time.LocalDateTime;

@Builder
@Data
public class ActRuTaskDto {
    @Schema(description = "主键")
    private String id;

    @Schema(description = "修订版本号(用于乐观锁控制)")
    private Integer rev;

    @Schema(description = "执行实例ID")
    private String executionId;

    @Schema(description = "流程实例ID")
    private String procInstId;

    @Schema(description = "流程定义ID")
    private String procDefId;

    @Schema(description = "任务定义ID")
    private String taskDefId;

    @Schema(description = "任务状态")
    private String state;

    @Schema(description = "任务名称")
    private String name;

    @Schema(description = "任务描述")
    private String description;

    @Schema(description = "任务定义键")
    private String taskDefKey;

    @Schema(description = "任务处理人")
    private String assignee;

    @Schema(description = "优先级")
    private Integer priority;

    @Schema(description = "创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    @Schema(description = "开始处理时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime inProgressTime;

    @Schema(description = "开始处理人")
    private String inProgressStartedBy;

    @Schema(description = "签收时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime claimTime;

    @Schema(description = "签收人")
    private String claimedBy;

    @Schema(description = "挂起时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime suspendedTime;

    @Schema(description = "挂起人")
    private String suspendedBy;

    @Schema(description = "处理中截止时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime inProgressDueDate;

    @Schema(description = "截止时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime dueDate;

    @Schema(description = "分类")
    private String category;

    @Schema(description = "挂起状态(1-活跃,2-挂起)")
    private Integer suspensionState;

    @Schema(description = "开始时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;

    @Schema(description = "结束时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;

    @Schema(description = "请假天数")
    private String days;

    @Schema(description = "请假理由")
    private String reason;
}

11.4 HandleTaskDto

package com.dragon.springboot3vue3.controller.flowable.dto.entityDto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
public class HandleTaskDto {

    @Schema(description = "开始时间")
    private String startTimeValue;

    @Schema(description = "结束时间")
    private String endTimeValue;

    @Schema(description = "请假天数")
    private float days;

    @Schema(description = "请假理由")
    private String reason;

    // 开启流程

    @Schema(description = "申请人ID")
    private String userId;

    @Schema(description = "流程定义Key")
    private String procDefKey;

    // 认领和处理任务

    @Schema(description = "审批者ID(认领任务者ID)")
    private String handleUserId;

    @Schema(description = "任务ID")
    private String managerApprovalTaskId;

    @Schema(description = "用户组ID")
    private String groupId;

    @Schema(description = "处理结果")
    private Boolean result;
}

11.5 PageDTO

package com.dragon.springboot3vue3.controller.dto.pageDto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

@Data
@Schema(description = "基础分页")
public class PageDTO {
    @NotNull
    @Schema(description = "当前页码")
    public Long currentPage;

    @NotNull
    @Schema(description = "每页记录数")
    public Long pageSize;
}

11.6 ActIdGroupPageDto

package com.dragon.springboot3vue3.controller.flowable.dto.pageDto;

import com.dragon.springboot3vue3.controller.dto.pageDto.PageDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
public class ActIdGroupPageDto extends PageDTO {
    @Schema(description = "组名")
    private String name;
}

11.7 ActIdUserPageDto

package com.dragon.springboot3vue3.controller.flowable.dto.pageDto;

import com.dragon.springboot3vue3.controller.dto.pageDto.PageDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
public class ActIdUserPageDto extends PageDTO {
    @Schema(description = "用户显示名称")
    private String displayName;

    @Schema(description = "用户组名")
    private String groupName;
}

11.8 ActReProcDefPageDto

package com.dragon.springboot3vue3.controller.flowable.dto.pageDto;

import com.dragon.springboot3vue3.controller.dto.pageDto.PageDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
public class ActReProcDefPageDto extends PageDTO {
    @Schema(description = "流程定义名称")
    private String name;
}

11.9 ProcessInstanceHistoryPageDto

package com.dragon.springboot3vue3.controller.flowable.dto.pageDto;

import com.dragon.springboot3vue3.controller.dto.pageDto.PageDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
public class ProcessInstanceHistoryPageDto extends PageDTO {
    @Schema(description = "申请人ID")
    private String userId;
}

12. 完整的流程处理代码(包含部署流程定义、开启流程实例、处理任务、查看历史任务等)

package com.dragon.springboot3vue3.controller.flowable;

import cn.dev33.satoken.util.SaResult;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dragon.springboot3vue3.controller.dto.commonDto.StringDTO;
import com.dragon.springboot3vue3.controller.flowable.dto.entityDto.ActHiProcinstDto;
import com.dragon.springboot3vue3.controller.flowable.dto.entityDto.ActRuTaskDto;
import com.dragon.springboot3vue3.controller.flowable.dto.entityDto.HandleTaskDto;
import com.dragon.springboot3vue3.controller.flowable.dto.pageDto.ActReProcDefPageDto;
import com.dragon.springboot3vue3.controller.flowable.dto.pageDto.ProcessInstanceHistoryPageDto;
import com.dragon.springboot3vue3.entity.flowable.ActIdMembership;
import com.dragon.springboot3vue3.entity.flowable.ActReProcDef;
import com.dragon.springboot3vue3.entity.flowable.ActRuTask;
import com.dragon.springboot3vue3.service.flowableService.ActIdMembershipService;
import com.dragon.springboot3vue3.service.flowableService.ActReProcDefService;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.flowable.common.engine.api.FlowableObjectNotFoundException;
import org.flowable.engine.*;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.history.HistoricProcessInstanceQuery;
import org.flowable.task.api.Task;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.flowable.variable.api.history.HistoricVariableInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Tag(name = "Flowable 流程引擎 API")
@RestController
@RequestMapping("/process")
public class ProcessController {
    @Autowired
    private TaskService taskService;
    @Autowired
    private RepositoryService repositoryService;
    @Autowired
    private RuntimeService runtimeService;
    @Autowired
    private IdentityService identityService;
    @Autowired
    private HistoryService historyService;
    @Autowired
    private ActReProcDefService actReProcDefService;
    @Autowired
    private ActIdMembershipService  actIdMembershipService;

    @Operation(summary = "上传文件部署流程定义")
    @PostMapping("/uploadFileDeploy")
    public SaResult uploadFileDeploy(@RequestParam("file") MultipartFile file) {
        try {
            repositoryService.createDeployment()
                    .addBytes(file.getOriginalFilename(), file.getBytes())
                    .name(file.getOriginalFilename())
                    .deploy();
            return SaResult.ok("流程部署成功 ");
        } catch (Exception e) {
            return SaResult.error("流程部署失败 ");
        }
    }

    @Operation(summary = "本地文件部署流程定义")
    @PostMapping("/localFileDeploy")
    public SaResult localFileDeploy() {
        // 本地文件部署,resources -> process -> leave-request.bpmn20.xml
        repositoryService.createDeployment()
                .addClasspathResource("process/leave-request.bpmn20.xml")
                .name("请假流程")
                .deploy();
        return SaResult.ok("部署成功");
    }

    @Operation(summary = "流程定义分页列表")
    @PostMapping("/list")
    public SaResult list(@RequestBody ActReProcDefPageDto pageDto) {
        // 创建分页对象
        Page<ActReProcDef> page = new Page<>(pageDto.getCurrentPage(), pageDto.getPageSize());

        // 构造多表查询条件
        MPJLambdaWrapper<ActReProcDef> qw = new MPJLambdaWrapper<ActReProcDef>()
                .like(StringUtils.isNotBlank(pageDto.getName()), ActReProcDef::getName, pageDto.getName());

        // 根据查询条件,将结果封装到分页对象
        Page<ActReProcDef> response = actReProcDefService.page(page, qw);

        return SaResult.ok().setData(response);
    }

    @Operation(summary = "流程定义删除")
    @DeleteMapping("/remove")
    public SaResult remove(@RequestBody StringDTO stringDTO) {
        // 根据 deploymentId 删除,是否级联删除(流程启动了也可以删除)
        repositoryService.deleteDeployment(stringDTO.getStr(), true);

        return SaResult.ok();
    }

    @Operation(summary = "挂起流程定义")
    @PostMapping("/suspend")
    public SaResult suspend(@RequestBody StringDTO stringDTO) {
        repositoryService.suspendProcessDefinitionByKey(stringDTO.getStr());
        return SaResult.ok("挂起流程定义");
    }

    @Operation(summary = "激活流程定义")
    @PostMapping("/activate")
    public SaResult activate(@RequestBody StringDTO stringDTO) {
        repositoryService.activateProcessDefinitionByKey(stringDTO.getStr());
        return SaResult.ok("激活流程定义");
    }

    /**
     * 一个流程实例通常会包含多个任务节点,这些任务会在流程执行过程中逐步生成和分配
     */
    @Operation(summary = "开启请假流程实例")
    @PostMapping("/startLeave")
    public SaResult startLeave(@RequestBody HandleTaskDto handleTaskDto) {
        // 设置启动人
        identityService.setAuthenticatedUserId(handleTaskDto.getUserId());

        // 请假实例参数
        Map<String, Object> variables = new HashMap<>();
        variables.put("userId", handleTaskDto.getUserId());
        variables.put("groupId", handleTaskDto.getGroupId());

        // 启动流程实例,并设置流程变量
        runtimeService.startProcessInstanceByKey(handleTaskDto.getProcDefKey(), variables);

        return SaResult.ok();
    }

    @Operation(summary = "删除流程实例历史数据")
    @DeleteMapping("/deleteHistoricProcessInstance")
    public SaResult deleteHistoricProcessInstance(@RequestParam String processInstanceId) {
        try {
            // 查询流程实例是否存在(包括历史和运行中)
            HistoricProcessInstance historicInstance = historyService.createHistoricProcessInstanceQuery()
                    .processInstanceId(processInstanceId)
                    .singleResult();

            if (historicInstance == null) {
                return SaResult.error("流程实例不存在");
            }

            // 如果流程在运行中,先删除运行中实例
            if (historicInstance.getEndTime() == null) {
                runtimeService.deleteProcessInstance(processInstanceId, "管理员删除");
            }
            // 再删除历史流程实例
            historyService.deleteHistoricProcessInstance(processInstanceId);
            return SaResult.ok("流程实例删除成功");
        } catch (FlowableObjectNotFoundException e) {
            return SaResult.error("指定的流程实例不存在");
        } catch (Exception e) {
            return SaResult.error("删除历史流程实例失败: " + e.getMessage());
        }
    }

    @Operation(summary = "查询用户待办任务")
    @GetMapping("/getUserTasks")
    public SaResult getUserTasks(@RequestParam String userId) {
        List<Task> tasks = taskService.createTaskQuery()
                .taskAssignee(userId)
                .list();

        List<ActRuTask> list = tasks.stream()
                .map(task -> ActRuTask.builder()
                        .id(task.getId())
                        .name(task.getName())
                        .assignee(task.getAssignee())
                        .createTime(DateUtil.toLocalDateTime(task.getCreateTime()))
                        .procInstId(task.getProcessInstanceId())
                        .procDefId(task.getProcessDefinitionId())
                        .formKey(task.getFormKey())
                        .build())
                .toList();

        return SaResult.ok().setData(list);
    }

    @Operation(summary = "用户处理任务")
    @PostMapping("/assigneeHandle")
    public SaResult assigneeHandle(@RequestBody HandleTaskDto handleTaskDto) {
        Task task = taskService.createTaskQuery()
                .taskAssignee(handleTaskDto.getUserId())
                .singleResult();

        // 请假实例参数
        Map<String, Object> variables = new HashMap<>();
        variables.put("userId", handleTaskDto.getUserId());
        variables.put("startTime", handleTaskDto.getStartTimeValue());
        variables.put("endTime", handleTaskDto.getEndTimeValue());
        variables.put("days", String.valueOf(handleTaskDto.getDays()));
        variables.put("reason", handleTaskDto.getReason());

        // 处理任务
        taskService.complete(task.getId(), variables);

        return SaResult.ok();
    }

    @Operation(summary = "认领用户处理认领的任务")
    @PostMapping("/handleClaimTask")
    public SaResult handleClaimTask(@RequestBody HandleTaskDto handleTaskDto) {
        Task task = taskService.createTaskQuery()
                .taskAssignee(handleTaskDto.getHandleUserId())
                .singleResult();

        if(task == null){
            return SaResult.error("任务已处理");
        }

        // 是否同意
        Map<String, Object> variables = new HashMap<>();
        variables.put("result", handleTaskDto.getResult());

        taskService.complete(task.getId(), variables);
        return SaResult.ok("任务处理完成");
    }

    @Operation(summary = "查询历史活动详情列表")
    @GetMapping("/getHistoryTask")
    public SaResult getHistoryTask(@RequestParam String processInstanceId) {
        List<HistoricActivityInstance> historicTasks = historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(processInstanceId)
                .orderByHistoricActivityInstanceEndTime().asc()
                .list();

        List<ActRuTaskDto> list = historicTasks.stream()
                .map(task -> ActRuTaskDto.builder()
                        .id(task.getActivityId())
                        .name(task.getActivityName())
                        .assignee(task.getAssignee())
                        .createTime(DateUtil.toLocalDateTime(task.getStartTime()))
                        .endTime(DateUtil.toLocalDateTime(task.getEndTime()))
                        .procInstId(task.getProcessInstanceId())
                        .procDefId(task.getProcessDefinitionId())
                        .build()
                ).toList();

        return SaResult.ok().setData(list);
    }

    @Operation(summary = "查询流程实例历史记录列表")
    @PostMapping("/getProcessInstanceHistory")
    public SaResult getProcessInstanceHistory(@RequestBody ProcessInstanceHistoryPageDto pageDto) {
        // 创建分页对象
        Page<ActHiProcinstDto> page = new Page<>(pageDto.getCurrentPage(),pageDto.getPageSize());

        // 创建查询对象
        HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery()
                .orderByProcessInstanceStartTime().desc();

        // 根据启动人筛选流程实例
        if (StringUtils.isNotBlank(pageDto.getUserId())) {
            query = query.startedBy(pageDto.getUserId());
        }

        // 查询当前页的数据
        List<HistoricProcessInstance> historicTasks = query.listPage((int) ((pageDto.getCurrentPage() - 1) * pageDto.getPageSize()), Math.toIntExact(pageDto.getPageSize()));

        // 转换为 DTO 列表
        List<ActHiProcinstDto> list = historicTasks.stream()
                .map(task -> {
                    // 获取变量
                    Map<String, Object> variables = getApplyVariables(task.getId());
                    // 获取任务ID
                    HistoricTaskInstance managerApprovalTask = historyService.createHistoricTaskInstanceQuery()
                            .processInstanceId(task.getId())
                            .taskDefinitionKey("managerApproval")
                            .unfinished()
                            .singleResult();

                    return ActHiProcinstDto.builder()
                            .id(task.getId())
                            .name(task.getName())
                            .startTime(DateUtil.toLocalDateTime(task.getStartTime()))
                            .endTime(DateUtil.toLocalDateTime(task.getEndTime()))
                            .procDefId(task.getProcessDefinitionId())
                            .procDefName(task.getProcessDefinitionName())
                            .procDefKey(task.getProcessDefinitionKey())
                            .procInstId(task.getId())
                            .startUserId(task.getStartUserId())
                            .startTimeValue( (String) variables.get("startTime"))
                            .endTimeValue((String) variables.get("endTime"))
                            .days((String) variables.get("days"))
                            .reason((String) variables.get("reason"))
                            .userId((String) variables.get("userId"))
                            .result(variables.get("result") != null ? (Boolean) variables.get("result") : null)
                            .managerApprovalTaskId(managerApprovalTask!= null ? managerApprovalTask.getId() : null)
                            .build();
                        }
                ).toList();

        // 查询总记录数
        long total = query.count();

        // 封装分页结果
        page.setRecords(list);
        page.setTotal(total);

        return SaResult.ok().setData(page);
    }

    @Operation(summary = "查询活跃任务列表(可认领和处理)")
    @GetMapping("/getActiveTask")
    public SaResult getActiveTask() {
        List<Task> tasks = taskService.createTaskQuery().active().list();

        List<ActRuTaskDto> list = tasks.stream().map(task -> {
            Map<String, Object> variables = taskService.getVariables(task.getId());

            // 将日期字符串转为LocalDateTime
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            LocalDateTime startTime = LocalDateTime.parse((CharSequence) variables.get("startTime"), formatter);
            LocalDateTime endTime = LocalDateTime.parse((CharSequence) variables.get("endTime"), formatter);

            return ActRuTaskDto.builder()
                    .id(task.getId())
                    .name(task.getName())
                    .assignee(task.getAssignee())
                    .createTime(DateUtil.toLocalDateTime(task.getCreateTime()))
                    .procInstId(task.getProcessInstanceId())
                    .procDefId(task.getProcessDefinitionId())
                    .startTime(startTime)
                    .endTime(endTime)
                    .days((String) variables.get("days"))
                    .reason((String) variables.get("reason"))
                    .build();
        }).toList();

        return SaResult.ok().setData(list);
    }

    @Operation(summary = "查询挂起的任务列表(任务暂停,不可处理)")
    @GetMapping("/getSuspendedTask")
    public SaResult getSuspendedTask() {
        List<Task> list = taskService.createTaskQuery().suspended().list();
        return SaResult.ok().setData(list);
    }

    @Operation(summary = "根据流程实例查询任务ID")
    @GetMapping("/getTaskId")
    public SaResult getTaskId(@RequestParam String processInstanceId) {
        String managerApprovalTaskId = historyService.createHistoricTaskInstanceQuery()
                .processInstanceId(processInstanceId)
                .taskDefinitionKey("managerApproval")
                .unfinished()
                .singleResult().getId();
        return SaResult.ok().setData(managerApprovalTaskId);
    }

    @Operation(summary = "查询历史记录中流程实例变量")
    @GetMapping("/getHistoryInstanceVariables")
    public SaResult getHistoryInstanceVariables(@RequestParam String processInstanceId) {
        return SaResult.ok().setData(getApplyVariables(processInstanceId));
    }

    @Operation(summary = "查询历史记录中流程实例变量")
    private Map<String, Object> getApplyVariables(String processInstanceId) {
        HistoricVariableInstance days = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("days")
                .singleResult();
        HistoricVariableInstance startTime = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("startTime")
                .singleResult();
        HistoricVariableInstance endTime = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("endTime")
                .singleResult();
        HistoricVariableInstance userId = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("userId")
                .singleResult();
        HistoricVariableInstance reason = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("reason")
                .singleResult();
        HistoricVariableInstance result = historyService.createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .variableName("result")
                .singleResult();

        HashMap<String, Object> map = new HashMap<>();
        map.put("days", days  != null ? days.getValue() : null);
        map.put("startTime", startTime  != null ? startTime.getValue() : null);
        map.put("endTime", endTime  != null ? endTime.getValue() : null);
        map.put("userId", userId  != null ? userId.getValue() : null);
        map.put("reason", reason != null ? reason.getValue() : null);
        map.put("result", result != null ? result.getValue() : null);
        return map;
    }

    @Operation(summary = "查询候选组任务")
    @GetMapping("/getGroupTasks")
    public SaResult getGroupTasks(@RequestParam String groupId) {
        List<Task> tasks = taskService.createTaskQuery()
                .taskCandidateGroup(groupId)
                .list();

        List<ActRuTaskDto> list = tasks.stream().map(task -> {
            Map<String, Object> variables = taskService.getVariables(task.getId());

            // 将日期字符串转为LocalDateTime
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            LocalDateTime startTime = LocalDateTime.parse((CharSequence) variables.get("startTime"), formatter);
            LocalDateTime endTime = LocalDateTime.parse((CharSequence) variables.get("endTime"), formatter);

            return ActRuTaskDto.builder()
                    .id(task.getId())
                    .name(task.getName())
                    .assignee(task.getAssignee())
                    .createTime(DateUtil.toLocalDateTime(task.getCreateTime()))
                    .procInstId(task.getProcessInstanceId())
                    .procDefId(task.getProcessDefinitionId())
                    .startTime(startTime)
                    .endTime(endTime)
                    .days((String) variables.get("days"))
                    .reason((String) variables.get("reason"))
                    .build();
        }).toList();

        return SaResult.ok().setData(list);
    }

    @Operation(summary = "认领候选组任务")
    @PostMapping("/claimGroupTask")
    public SaResult claimGroupTask(@RequestBody HandleTaskDto handleTaskDto) {
        // 判断用户是否属于该用户组
        ActIdMembership one = actIdMembershipService.lambdaQuery()
                .eq(ActIdMembership::getUserId, handleTaskDto.getHandleUserId())
                .eq(ActIdMembership::getGroupId, handleTaskDto.getGroupId())
                .one();
        if(one == null){
            return SaResult.error("您无权限认领此任务");
        }
        // 认领任务
        taskService.claim(handleTaskDto.getManagerApprovalTaskId(), handleTaskDto.getHandleUserId());
        return SaResult.ok("任务已认领");
    }
}

flowable是一个开源的、可扩展的业务流程管理引擎。它基于Java语言开发,可以与Spring框架无缝集成flowable6.7.2flowable的一个版本,在该版本中修复了一些已知的bug,并加入了一些新特性和改进。该版本的flowable可以在SpringBoot应用程序中使用。 SpringBoot是一个能够简化Spring应用开发的框架。它使用约定优于配置的原则,通过自动化配置和快速启动来减少开发者的工作量。使用SpringBoot可以快速搭建和部署应用程序,并且可以与各种流行的开发框架和技术无缝集成Vue是一种用于构建用户界面的渐进式JavaScript框架。它主要用于构建单页面应用程序,可以通过组件化方式构建复杂的用户界面。Vue具有简单易用、灵活、高效等特点,支持双向数据绑定和组件化的开发模式。VueSpringBoot可以通过RESTful API进行交互,实现前后端分离开发。 当使用flowable6.7.2时,可以将其集成SpringBoot应用程序中,以便在应用中使用业务流程管理功能。可以通过引入适当的依赖和进行配置来实现集成。同时,可以使用Vue来构建应用程序的用户界面,通过向后端发送请求和接收响应来实现与flowable的交互。可以通过调用flowable的API来管理和执行业务流程,并将结果通过RESTful API返回给前端的Vue组件进行展示和交互。 总而言之,flowable6.7.2可以与SpringBootVue无缝集成,实现一个具备业务流程管理功能的应用程序。SpringBoot提供了后端的支持,Vue提供了前端的支持,而flowable则负责业务流程的管理和执行。这样的架构可以提高开发效率和应用程序的稳定性,使开发者能够更加专注于业务逻辑的实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值