仿activiti实现一个简版工作流

前言

本文代码实现是仿照工作流实现的,由于业务需求的特殊性,没有直接使用工作流。

功能实现

功能实现
功能说明
任务查询1.个人任务查询
2.节点任务查询
3.组内任务查询
上述3种查询可组合查询
任务驳回1.驳回到任意节点
2.节点跳跃(可忽略驳回点与驳回至节点之间的任意节点)
任务分配(拾取)将任务分配给某个人
任务归还这个操作在项目中没有具体使用,不过已经实现相关功能
任务删除1.删除当前任务
2.删除任务流转记录
支持强制删除任务(默认进行中的任务无法删除),自由设置任务流程记录是否删除
流程记录1.流程记录查询
2.流程记录添加
3.流程记录删除
1.责任人与组为一对多关系
2.不支持网关相关逻辑

1.需求及实现

以下根据实际项目进行需求举例说明,以下截图为功能部分截图,仅供参考

需求说明实现效果
1️⃣业务流程设置1.管理员可以对任意流程节点进行开启关闭操作
2.节点设置,仅对之后的流程实例生效,进行中的仍按之前的流程节点进行
2️⃣项目权限(节点任务组)设置1.为不同的人员 ,在项目节点下设置不同的任务组权限
将这个功能带入到工作流中,可以理解为在某个节点设置任务组,把人员分到不同的组中,只有当前组下的人员才能看到任务

说明: 此图仅供参考,为本人实际项目部分图,项目权限为流程权限,其中包含了非流程节点。
1.项目权限:表示流程节点
2.类别名称:可以理解为任务 组。

2.代码实现

1.流程设置

这里的流程设置,相当于画任务流程节点图,我是将节点都放在了数据字典中在这里插入图片描述
然后将需要的节点存储即可,这里比较简单,就不列代码了。

2.项目权限设置

流程节点下任务组,责任人设置

1.表设计

在这里插入图片描述
operator_auth,用于存放当前检测项目(任务组)下不同节点的权限。这个不是本文的重点,这里就不列出具体的代码了。

3.任务流程处理

1.表设计

1.detection_flow_task

detection_flow_task 任务主表
类型 注释
model_type varchar(255) 流程节点
test_code varchar(255) 检测项目编号(任务组)
business_key bigint(20) 业务id
signee bigint(20) 责任人
variable varchar(1000)变量
next_node varchar(255)下个节点
operate int(11)操作类型 0驳回 1通过

2.detection_flow_nodes
存储任务流程实时节点数据

detection_flow_task 任务主表
类型 注释
model_type varchar(255)任务节点
business_key bigint(20) 业务id

3.detection_flow_his

detection_flow_his 任务流程历史记录
类型 注释
model_type varchar(255)流程节点
business_key bigint(20) 业务id
variable varchar(1000)变量
next_node varchar(255)下个节点
operate int(11)操作类型 0驳回 1通过

2.代码实现

代码较多,这里只贴出核心代码
1.FlowTaskController

@Tag(name = "管理后台 - 任务流程-主表-当前任务")
@RestController
@RequestMapping("/detection/flow-task")
@Validated
public class FlowTaskController {
    @Resource
    private FlowTaskService flowTaskService;
    
    /**
     * @description发起流程 ,默认从起始节点开始
     * @author lvyq
     * @throws
     * @time 2023-09-15 10:10
     */
    @PostMapping("/startFlow")
    @Operation(summary = "发起任务流程")
    public CommonResult startFlow(@Valid @RequestBody FlowTaskCreateReqVO completeVO){
        flowTaskService.startFlow(completeVO.getBusinessKey(),completeVO.getSignee(),completeVO.getStartNode(),completeVO.getTestCode());
        return success(true);
    }
    
    /**
     * @description  完成当前流程
     * @author lvyq
     * @param[1] createReqVO
     * @throws
     * @return CommonResult<Long>
     * @time 2023-08-15 11:50
     */

    @PutMapping("/complete")
    @Operation(summary = "完成任务")
    public CommonResult<Boolean> complete(@Valid @RequestBody FlowTaskCompleteVO completeVO) {
        flowTaskService.complete(completeVO);
        return success(true);
    }
    /**
     * @description 任务驳回
     * @author lvyq
     * @param[1] completeVO
     * @throws
     * @return CommonResult<Boolean>
     * @time 2023-08-22 13:33
     */
    @PutMapping("/reject")
    @Operation(summary = "任务驳回")
    public CommonResult<Boolean> reject(@Valid @RequestBody FlowTaskRejectVO vo) {
        flowTaskService.reject(vo);
        return success(true);
    }
    /**
     * @description 获取当前节点的待办任务
     * @author lvyq
     * @param[1] pageVO
     * @throws
     * @return CommonResult<PageResult<FlowTaskRespVO>>
     * @time 2023-08-15 15:03
     */
    @GetMapping("/page")
    @Operation(summary = "获得任务流程-主表-当前任务分页")
    @TaskFlowAuth
    public CommonResult getFlowTaskPage(@Valid FlowTaskPageReqVO pageVO) {
        return success(flowTaskService.getFlowTaskPage(pageVO));
    }
    /**
     * @description
     * @author lvyq
     * @param[1] pageVO 指派任务
     * @throws
     * @return CommonResult
     * @time 2023-09-25 14:25
     */
    @PutMapping("/assignTask")
    public CommonResult assignTask(@Valid @RequestBody AssignTaskVo completeVO){
        flowTaskService.assignTask(completeVO);
        return success(true);
    }

    /**
     * @description  重新发起流程
     * @author lvyq
     * @param[1] completeVO
     * @throws
     * @return CommonResult
     * @time 2023-09-26 15:05
     */
    @PutMapping("/reStartFlow")
    @Operation(summary = "重新发起任务流程")
    public void reStartFlow(@Valid @RequestBody ReStartFlowTaskVo vo){
        flowTaskService.reStartFlow(vo.getBusinessKey(),vo.getDelHis());
    }
}

2.FlowTaskServiceImpl

@Service
@Validated
public class FlowTaskServiceImpl implements FlowTaskService {

    @Resource
    private FlowTaskMapper flowTaskMapper;

    @Resource
    private FlowHisMapper flowHisMapper;
    @Resource
    private FlowHisService flowHisService;

    @Resource
    private FlowNodesMapper flowNodesMapper;


    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void deleteFlowTask(Long id) {
        // 校验存在
        validateFlowTaskExists(id);
        // 删除
        flowTaskMapper.deleteById(id);
    }

    private void validateFlowTaskExists(Long id) {
        if (flowTaskMapper.selectById(id) == null) {
            throw exception(FLOW_TASK_NOT_EXISTS);
        }
    }

    @Override
    public FlowTaskDO getFlowTask(Long id) {
        return flowTaskMapper.selectById(id);
    }

    @Override
    public List<FlowTaskDO> getFlowTaskList(Collection<Long> ids) {
        return flowTaskMapper.selectBatchIds(ids);
    }

    @Override
    public PageResult<FlowTaskRespVO> getFlowTaskPage(FlowTaskPageReqVO pageReqVO) {
        IPage<FlowTaskRespVO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
        List<FlowTaskRespVO> flowTaskDOList = flowTaskMapper.getFlowTaskPageForDetection(page,pageReqVO);
        List<String> nodes= getNodes();
        flowTaskDOList.forEach(ls->{
            Map<String,Object> map = new HashMap<>();
            //历史节点
            String flowNodes = ls.getFlowNodes();
            if (!StringUtils.isEmpty(flowNodes)){
                String[] split = flowNodes.split(",");
                List<String> list = Arrays.asList(split);
                nodes.forEach(no->{
                    if (list.contains(no)){
                        map.put(no,1);
                    }else {
                        map.put(no,0);
                    }
                });
            } else {
                nodes.forEach(node->{
                    map.put(node,0);
                });
            }
            //设置当前节点为否
            map.put(ls.getModelType(),0);
            ls.setFlowStatus(map);
        });
        return new PageResult<>(flowTaskDOList,page.getTotal());

    }


    /**
     * @return boolean
     * @throws
     * @description 检测任务是否存在
     * @author lvyq
     * @time 2023-08-15 13:42
     */
    public boolean checkTask(Long businessKey) {
        boolean state = false;
        FlowTaskDO flowTaskDO = flowTaskMapper.selectOne(FlowTaskDO::getBusinessKey, businessKey);
        if (flowTaskDO != null) state = true;
        return state;
    }


    /**
     * @throws
     * @description
     * @author lvyq  审核通过
     * @param[1] completeVO
     * @time 2023-08-22 10:52
     */
    @Override
    @Transactional
    public void  complete(FlowTaskCompleteVO completeVO) {
        //获取当前任务
        FlowTaskDO taskDO = validateFlowTask(completeVO.getBusinessKey());
        //信息补全
        taskDO.setId(null);
        taskDO.setVariable(completeVO.getVariable());
        taskDO.setRemark(completeVO.getRemark());
        taskDO.setSignee(completeVO.getSignee());
        taskDO.setOperate(1);
        //流程节点
        List<String> nodes = flowNodesMapper.selectOne(FlowNodesDO::getBusinessKey, completeVO.getBusinessKey()).getModelType();
        FlowHisCreateReqVO flowHisCreateReqVO = BeanUtil.copyProperties(taskDO, FlowHisCreateReqVO.class);
        //当前流程没有下个环节,则表示流程通过后即结束当前流程
        flowTaskMapper.deleteByBusinessKey(taskDO.getBusinessKey());
        if (StringUtils.isEmpty(taskDO.getNextNode())) {
            flowHisCreateReqVO.setRemark("流程结束");
        } else {
            //前往下个流程
            completeTask(nodes, taskDO);
        }
        flowHisService.createFlowHis(flowHisCreateReqVO);
    }

    /**
     * @throws
     * @description 审核通过处理
     * @author lvyq
     * @time 2023-09-15 9:47
     */
    private void completeTask(List<String> nodes, FlowTaskDO taskDO) {
        //即将前往的流程节点
        int currentNodeIndex = getNodeIndex(nodes,taskDO.getNextNode());
        taskDO.setModelType(taskDO.getNextNode());
        if (currentNodeIndex >= nodes.size() - 1) {
            //即将前往的节点为流程的最后一个节点,则不存在下个节点
            taskDO.setNextNode(null);
        } else {
            taskDO.setNextNode(nodes.get(currentNodeIndex + 1));
        }
        flowTaskMapper.insert(taskDO);
    }


    /**
     * @throws
     * @description 审核驳回
     * @author lvyq
     * @param[1] completeVO
     * @time 2023-08-22 13:45
     */
    @Override
    @Transactional
    public void reject(FlowTaskRejectVO completeVO) {
        taskValCheck(completeVO);
        //获取当前任务
        FlowTaskDO taskDO = validateFlowTask(completeVO.getBusinessKey());
        //处理最新的流程节点集合
        List<String> nodes = checkCurrentNodes(completeVO);
        // variable 设置
        taskDO.setVariable(completeVO.getVariable());
        taskDO.setId(null);
        taskDO.setRemark(completeVO.getRemark());
        taskDO.setOperate(0);//驳回
        FlowHisCreateReqVO flowHisCreateReqVO = BeanUtil.copyProperties(taskDO, FlowHisCreateReqVO.class);
        //历史记录节点处理
        //1.发起节点 为未变更时taskDO的当前任务节点,下个节点为变更后的taskDo的当前节点
        if (!StringUtils.isEmpty(completeVO.getRejectModelType())) {
            if (!nodes.contains(completeVO.getRejectModelType())) {
                throw exception(new ErrorCode(10001, "驳回节点不存在,驳回失败"));
            }
            //指定回退节点
            rejectSpecifyNode(nodes, taskDO, completeVO.getRejectModelType(), completeVO.getIsInOrder());
        } else {
            //按序驳回,禁止忽略流程节点操作,不对相应数据进行处理
            rejectInOrder(nodes, taskDO);
        }
        //删除当前任务数据
        flowTaskMapper.deleteByBusinessKey(taskDO.getBusinessKey());
        flowTaskMapper.insert(taskDO);
        //flowHisCreateReqVO.setOperate(0);
        flowHisCreateReqVO.setNextNode(taskDO.getModelType());
        flowHisService.createFlowHis(flowHisCreateReqVO);
    }

    /**
     * @return
     * @throws
     * @description 发起流程
     * @author lvyq
     * @param[1] businessKey 业务id
     * @param[2] signee  责任人
     * @param[3] startNode 流程起始节点,缺省时默认第一个节点
     * @time 2023-09-15 10:43
     */
    @Override
    public void startFlow(Long businessKey, Long signee, String startNode, String testCode) {
        //查询当前流程是否存在
        if (checkTask(businessKey)){
            throw exception(FLOW_IS_CREATED);
        }
        List<String> nodes = getNodes();
        FlowNodesDO nodesDO = new FlowNodesDO();
        //流程节点存储
        nodesDO.setBusinessKey(businessKey);
        nodesDO.setModelType(nodes);
        //任务主表
        FlowTaskDO flowTask = new FlowTaskDO();
        flowTask.setSignee(signee);
        flowTask.setBusinessKey(businessKey);
        //通过
        flowTask.setOperate(1);
        //检测项目
        flowTask.setTestCode(testCode);
        FlowHisCreateReqVO flowHisCreateReqVO = BeanUtil.copyProperties(flowTask, FlowHisCreateReqVO.class);
        if (startNode != null) {
            flowTask.setModelType(startNode);
            //下个节点获取 ,即将前往的流程节点
            int currentNodeIndex = getNodeIndex(nodes,startNode);
            if (currentNodeIndex >= nodes.size() - 1) {
                //即将前往的节点为流程的最后一个节点,则不存在下个节点
                flowTask.setNextNode(null);
            } else {
                flowTask.setNextNode(nodes.get(currentNodeIndex + 1));
            }
        } else {
            flowTask.setModelType(nodes.get(0));
            flowTask.setNextNode(nodes.get(1));
        }
        //创建任务记录
        flowTaskMapper.insert(flowTask);
        flowNodesMapper.insert(nodesDO);
        //流程历史记录处理补全

        //起始环节为空
        flowHisCreateReqVO.setModelType(null);
        //历史记录中的下个环节设置为当前所在的环节-需求设计
        flowHisCreateReqVO.setNextNode(flowTask.getModelType());
        flowHisCreateReqVO.setRemark("发起流程");

        flowHisService.createFlowHis(flowHisCreateReqVO);
    }





    /**
     * @description
     * @author lvyq
     * @param[1] completeVO
     * @throw 接口增强,追加判断,当前判断方法可有可无,之后的实现已根据该方法中的逻辑进行了相应处理,
     * 此方法仅做为接口增强
     * @time 2023-09-15 9:20
     */
    private void taskValCheck(FlowTaskRejectVO completeVO) {
        Integer isInOrder = completeVO.getIsInOrder();
        if (completeVO.getRejectModelType() != null) {
            //指定节点退回,
            //isOrder允许缺省,缺省时默认为1,按序流转  0 非按序流转
            if (isInOrder != null &&  isInOrder != 1 && isInOrder != 0) {
                throw exception(new ErrorCode(20230915, "非法参数{isInOrder}"));
            }
        } else {
            //非指定节点退回,判断,【需求设计】不允许移除流程节点,isInOrder缺省处理.以下逻辑已在相应实现中进行了处理,此处仅作为提示使用
            if (completeVO.getExcludeModelType() != null && completeVO.getExcludeModelType().size() > 0) {
                throw exception(new ErrorCode(20230915, "非法参数{excludeModelType}"));
            }
            //isInOrder 缺省处理即可,{1} 按序
            if (isInOrder != null) {
                throw exception(new ErrorCode(20230915, "非法参数{isInOrder}"));
            }

        }
    }


    /**
     * @throws
     * @description
     * @author lvyq
     * 按顺驳回
     * @time 2023-09-15 9:10
     */
    private void rejectInOrder(List<String> nodes, FlowTaskDO taskDO) {
        //非指定回退 默认退回上一步 下标位置
        int oldNodeIndex = getNodeIndex(nodes,taskDO.getModelType());
        //当前节点
        if (oldNodeIndex == 0) {
            //首节点,无法驳回
            throw exception(FLOW_ILLEGAL_OPERATION);
        } else {
            taskDO.setNextNode(nodes.get(oldNodeIndex));
            taskDO.setModelType(nodes.get(oldNodeIndex - 1));
        }
    }

    /**
     * @throws
     * @description
     * @author lvyq
     * 驳回到指定节点
     * @time 2023-09-15 8:45
     */
    private void rejectSpecifyNode(List<String> nodes, FlowTaskDO taskDO, String rejectModelType, Integer isInOrder) {
        //将要驳回至的节点
        String nextNode = taskDO.getModelType();
        taskDO.setModelType(rejectModelType);
        if (isInOrder == 0) {
            //非顺序 即回到驳回点
            taskDO.setNextNode(nextNode);
        } else {
            //按序
            int nodeIndex = getNodeIndex(nodes,rejectModelType);
            taskDO.setNextNode(nodes.get(nodeIndex + 1));
        }
    }

    /**
     * @return
     * @throws
     * @description
     * @author lvyq
     * 校验数据是否存在,存在则返回信息
     * @time 2023-09-14 17:24
     */
    private FlowTaskDO validateFlowTask(@NotNull(message = "businessKey不能为空") Long businessKey) {
        FlowTaskDO taskDO = flowTaskMapper.selectOne(FlowTaskDO::getBusinessKey, businessKey);
        if (taskDO == null) throw exception(FLOW_TASK_NOT_EXISTS);
        return taskDO;
    }

    /**
     * @return List<String>
     * @throws
     * @description
     * @author lvyq
     * 最新流程节点处理
     * @time 2023-09-14 17:03
     */
    private List<String> checkCurrentNodes(FlowTaskRejectVO completeVO) {
        //TODO 流程节点,获取全部流程节点
        List<String> nodesBefore = getNodes();
        List<String> currentNodes = new ArrayList<>();
        if (completeVO.getExcludeModelType() != null && completeVO.getExcludeModelType().size() > 0) {
            //更新流程节点
            nodesBefore.stream().filter(str -> !completeVO.getExcludeModelType().contains(str)).forEach(currentNodes::add);
        } else {
            currentNodes = nodesBefore;
        }
        //bug -2023.09.18  解决通过其它节点,未设置移除的节点时,仍使用上一个驳回所设置的流程节点问题。每次驳回都进行流程节点设置
        updateFlowNodes(currentNodes,completeVO.getBusinessKey());
        return currentNodes;
    }


    /**
     * @throws
     * @description
     * @author lvyq
     * 更新FlowNodes ,用于在审核通过时查询最新的流程
     * @time 2023-09-14 17:10
     */
    private void updateFlowNodes(@NotNull(message = "非法操作,流程节点不可为空") List<String> nodes, @NotNull(message = "onlineDataId不能为空") Long onlineDataId) {
        FlowNodesDO nodesDO = flowNodesMapper.selectOne(FlowNodesDO::getBusinessKey, onlineDataId);
        nodesDO.setModelType(nodes);
        flowNodesMapper.updateById(nodesDO);
    }

    private List<String> getNodes() {
        List<String> nodes = new ArrayList<>();
        String flowNodes = stringRedisTemplate.opsForValue().get("flowNode:"+SecurityFrameworkUtils.getLoginUser().getTenantId().toString());
        String[] split = flowNodes.split(",");
        for (String s : split) {
            nodes.add(s);
        }
        return nodes;
    }

    /**
     * @description 任务指派 -当前节点任务指派
     * @author lvyq
     * @param[1] completeVO
     * @throws

     * @time 2023-09-25 14:26
     */
    @Override
    public void assignTask(AssignTaskVo completeVO) {
        FlowTaskDO taskDO = validateFlowTask(completeVO.getBusinessKey());
        //修改数据
        taskDO.setSignee(completeVO.getSignee());
        flowTaskMapper.updateById(taskDO);
    }

    /**
     * @description  获取节点所在的下标
     * @author lvyq
     * @throws
     * @return Integer
     * @time 2023-09-26 11:40
     */
    public Integer getNodeIndex(List<String> nodes,String node){
        int[] nodeIndexArr = ListUtil.indexOfAll(nodes, node::equals);
        int nodeIndex = nodeIndexArr[0];
        return nodeIndex;
    }

    /**
     * @description 重新发起流程-逻辑抽离
     * @author lvyq
     * @param[1] businessKey
     * @param[2] delHis
     * @throws
     * @time 2023-09-26 15:23
     */
    @Override
    @Transactional
    public void reStartFlow(Long businessKey, Integer delHis) {
        //TODO 判断节点, 需求 - 审核节点,待优化成可配选项
        String reSetNode="doExamine";
        //获取当前任务节点
        FlowTaskDO taskDO = flowTaskMapper.selectOne(FlowTaskDO::getBusinessKey, businessKey);
        FlowHisCreateReqVO flowHisCreateReqVO = BeanUtil.copyProperties(taskDO, FlowHisCreateReqVO.class);
        //存在任务节点,判断是否需要修改节点
        if (taskDO!=null){
            String modelType = taskDO.getModelType();
            //判断当前节点是否是审核后的节点,如果是审核后的则将流程数据流转至审核节点上,同时将业务流转节点重置
            List<String> nodes = flowNodesMapper.selectOne(FlowNodesDO::getBusinessKey, businessKey).getModelType();
            Integer nodeIndex = getNodeIndex(nodes, modelType);
            Integer reSetIndex = getNodeIndex(nodes, reSetNode);
            if (delHis==1){
                flowHisMapper.deleteByBusinessKey(businessKey);
            }
            if (nodeIndex<=reSetIndex){
                flowHisCreateReqVO.setNextNode(taskDO.getModelType());
                flowHisCreateReqVO.setModelType(null);
                flowHisCreateReqVO.setRemark("试验修改【流程不变】");

            }else {
                //重置
                flowTaskMapper.deleteByBusinessKey(businessKey);
                flowNodesMapper.deleteByBusinessKey(businessKey);

                //流程节点存储
                List<String> newNodes = getNodes();
                FlowNodesDO nodesDO = new FlowNodesDO();
                nodesDO.setBusinessKey(businessKey);
                nodesDO.setModelType(newNodes);
                //任务主表
                FlowTaskDO flowTask = new FlowTaskDO();
                flowTask.setBusinessKey(businessKey);
                flowTask.setTestCode(taskDO.getTestCode());
                Integer nodeIndex1 = getNodeIndex(newNodes, reSetNode);
                flowTask.setModelType(newNodes.get(nodeIndex1));
                flowTask.setNextNode(newNodes.get(nodeIndex1+1));

                //创建任务记录
                flowTaskMapper.insert(flowTask);
                flowNodesMapper.insert(nodesDO);
                //起始环节为老数据的当前环节
                flowHisCreateReqVO.setModelType(taskDO.getModelType());
                //下个环节设置为当前所在的环节-需求设计
                flowHisCreateReqVO.setNextNode(flowTask.getModelType());
                flowHisCreateReqVO.setRemark("试验修改【流程变更】");
                }
            flowHisService.createFlowHis(flowHisCreateReqVO);

        }
        }
}

4.任务流程记录

任务流程记录比较简单,只是简单的增删改查。就不具体列举了。
在这里插入图片描述

5.接口说明

主要接口功能说明,更多操作看service

1️⃣ 发起流程

/startFlow发起流程
参数 说明
businessKey 业务key
signee 处理人
startNode 流程起始节点,缺省 nodes[0]
testCode 项目编号(任务组)
2️⃣ 完成当前流程
/complete完成当前流程
参数 说明
businessKey 业务key
signee 处理人(可缺省)
startNode 流程起始节点,缺省 nodes[0]
variable变量
3️⃣任务驳回
/reject任务驳回
参数 说明
businessKey 业务key
variable变量
isInOrder是否按序流转
0 否 ,退回至某个节点>完成>流转至退回前节点 权重为1
1.是, 按序流转,配合 excludeModelType 为按序流转节点中需要剔除的流程节点
rejectModelType指定退回节点 缺省,驳回到上个节点,并按序流转
excludeModelType 剔除节点,与 isInOrder=1 时,搭配使用 缺省时按1处理
4️⃣任务代办列表
/page任务代办列表
参数 说明
businessKey 业务key
operate操作类型 0 驳回 1 通过
modelType流程节点
testCode任务组
5️⃣任务指派
/assignTask任务指派
参数 说明
businessKey 业务key
signee被指派人
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不要喷香水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值