【SpringBoot技术专题】「StateMachine」状态机设计及实现

简介: 【SpringBoot技术专题】「StateMachine」状态机设计及实现

有限状态机(Finite-state machine)


有限状态机(英语:finite-state machine,缩写:FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型


应用FSM模型可以帮助对象生命周期的状态的顺序以及导致状态变化的事件进行管理将状态和事件控制从不同的业务Service方法的if else中抽离出来。FSM的应用范围很广,对于有复杂状态流,扩展性要求比较高的场景都可以使用该模型。


下面是状态机模型中的4个要素,即现态、条件、动作、次态。


  • 现态:是指当前所处的状态。
  • 条件:又称为“事件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
  • 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态
  • 次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了


状态机中,每个状态有着相应的行为,随着行为的触发来切换状态。其中一种做法是使用二维数组实现状态机机制,其中横坐标表示行为,纵坐标表示状态,具体的数值则表示当前的状态。

我们以登录场景设计一个状态机。

image.png


设计一张状态机表。


横轴是动作,纵轴是状态

image.png

此时它的二维数组,如下所示

image.png


  • 此外,我们也可以通过状态模式实现一个状态机,状态模式将每一个状态封装成独立的类,具体行为会随着内部状态而改变。状态模式用类表示状态,这样我们就能通过切换类来方便地改变对象的状态,避免了冗长的条件分支语句,


  • 让系统具有更好的灵活性和可扩展性。现在,我们定义一个状态枚举,其中包括未连接、已连接、注册中、已注册 4 种状态。


image.png

定义一个环境类,它是实际上是真正拥有状态的对象。

image.png

状态模式用类表示状态,这样就能通过切换类来方便地改变对象的状态。我们定义几个状态类。

image.png

image.png

image.png

image.png


注意的是,如果某个行为不会触发状态的变化,我们可以抛出一个 RuntimeException 异常。此外,调用时,通过环境类控制状态的切换,如下所示。

image.png



Spring StateMachine 让状态机结构更加层次化,可以帮助开发者简化状态机的开发过程。现在,我们来用 Spring StateMachine 进行改造。修改 pom 文件,添加 Maven/gradle 依赖。

dependencies {
    compile 'org.springframework.statemachine:spring-statemachine-core:1.2.7.RELEASE'
}  
复制代码



定义一个状态枚举,其中包括未连接、已连接、注册中、已注册 4 种状态。

public enum RegStatusEnum {
    // 未连接
    UNCONNECTED,
    // 已连接
    CONNECTED,
    // 正在登录
    LOGINING,
    // 登录进系统
    LOGIN_INTO_SYSTEM;
}
复制代码



定义事件枚举,事件的发生触发状态转换

public enum RegEventEnum {
    // 连接
    CONNECT,
    // 开始登录
    BEGIN_TO_LOGIN,
    // 登录成功
    LOGIN_SUCCESS,
    // 登录失败
    LOGIN_FAILURE,
    // 注销登录
    LOGOUT;
}
复制代码



配置状态机,通过注解打开状态机功能。 配置类一般要继承EnumStateMachineConfigurerAdapter类,并且重写一些configure方法以配置状态机的初始状态以及事件与状态转移的联系。

import static com.qyz.dp.state.events.RegEventEnum.BEGIN_TO_LOGIN;
import static com.qyz.dp.state.events.RegEventEnum.CONNECT;
import static com.qyz.dp.state.events.RegEventEnum.LOGIN_FAILURE;
import static com.qyz.dp.state.events.RegEventEnum.LOGIN_SUCCESS;
import static com.qyz.dp.state.events.RegEventEnum.LOGOUT;
import static com.qyz.dp.state.state.RegStatusEnum.CONNECTED;
import static com.qyz.dp.state.state.RegStatusEnum.LOGINING;
import static com.qyz.dp.state.state.RegStatusEnum.LOGIN_INTO_SYSTEM;
import static com.qyz.dp.state.state.RegStatusEnum.UNCONNECTED;
import java.util.EnumSet;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import com.qyz.dp.state.events.RegEventEnum;
import com.qyz.dp.state.state.RegStatusEnum;
@Configuration
@EnableStateMachine // 开启状态机配置
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter{
    /**
     * 配置状态机状态
     */
    @Override
    public void configure(StateMachineStateConfigurer states) throws Exception {
        states.withStates()
        // 初始化状态机状态
        .initial(RegStatusEnum.UNCONNECTED)
        // 指定状态机的所有状态
        .states(EnumSet.allOf(RegStatusEnum.class));
    }
    /**
     * 配置状态机状态转换
     */
    @Override
    public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
        // 1. connect UNCONNECTED -> CONNECTED
        transitions.withExternal()
            .source(UNCONNECTED)
            .target(CONNECTED)
            .event(CONNECT)
        // 2. beginToLogin CONNECTED -> LOGINING
        .and().withExternal()
            .source(CONNECTED)
            .target(LOGINING)
            .event(BEGIN_TO_LOGIN)
        // 3. login failure LOGINING -> UNCONNECTED
        .and().withExternal()
            .source(LOGINING)
            .target(UNCONNECTED)
            .event(LOGIN_FAILURE)
        // 4. login success LOGINING -> LOGIN_INTO_SYSTEM
        .and().withExternal()
            .source(LOGINING)
            .target(LOGIN_INTO_SYSTEM)
            .event(LOGIN_SUCCESS)
        // 5. logout LOGIN_INTO_SYSTEM -> UNCONNECTED
        .and().withExternal()
            .source(LOGIN_INTO_SYSTEM)
            .target(UNCONNECTED)
            .event(LOGOUT);
    }
}
复制代码



Spring StateMachine 提供了注解配置实现方式,所有 StateMachineListener 接口中定义的事件都能通过注解的方式来进行配置实现。这里以连接事件为案例,@OnTransition 中 source 指定原始状态,target 指定目标状态,当事件触发时将会被监听到从而调用 connect() 方法。


在启动springboot时,需要注入状态机的状态,事件的配置。起主要涉及到以下两个类:


  • StateMachineStateConfigurer < S, E> 配置状态集合以及初始状态,泛型参数S代表状态,E代表事件。
  • StateMachineTransitionConfigurer 配置状态流的转移,可以定义状态转换接受的事件。


配置事件监听器,事件发生时会触发的操作

import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.WithStateMachine;
@Configuration
@WithStateMachine
public class StateMachineEventConfig {
    @OnTransition(source = "UNCONNECTED", target = "CONNECTED")
    public void connect() {
        System.out.println("Switch state from UNCONNECTED to CONNECTED: connect");
    }
    @OnTransition(source = "CONNECTED", target = "LOGINING")
    public void beginToLogin() {
        System.out.println("Switch state from CONNECTED to LOGINING: beginToLogin");
    }
    @OnTransition(source = "LOGINING", target = "LOGIN_INTO_SYSTEM")
    public void loginSuccess() {
        System.out.println("Switch state from LOGINING to LOGIN_INTO_SYSTEM: loginSuccess");
    }
    @OnTransition(source = "LOGINING", target = "UNCONNECTED")
    public void loginFailure() {
        System.out.println("Switch state from LOGINING to UNCONNECTED: loginFailure");      
    }
    @OnTransition(source = "LOGIN_INTO_SYSTEM", target = "UNCONNECTED")
    public void logout()
    {
        System.out.println("Switch state from LOGIN_INTO_SYSTEM to UNCONNECTED: logout");
    }
}
复制代码



通过注解自动装配一个状态机 这里写了一个rest接口来触发状态机变化

@RestController
public class WebApi {
    @Autowired
    private StateMachine stateMachine;
    @GetMapping(value = "/testStateMachine")
    public void testStateMachine()
    {
        stateMachine.start();
        stateMachine.sendEvent(RegEventEnum.CONNECT);
        stateMachine.sendEvent(RegEventEnum.BEGIN_TO_LOGIN);
        stateMachine.sendEvent(RegEventEnum.LOGIN_FAILURE);
        stateMachine.sendEvent(RegEventEnum.LOGOUT);
    }
}
Switch state from UNCONNECTED to CONNECTED: connect
Switch state from CONNECTED to LOGINING: beginToLogin
Switch state from LOGINING to UNCONNECTED: loginFailure
复制代码



  • 从输出可以看到,虽然send了4个事件,但只有三条输出。原因是最后一个LOGOUT事件发生时,状态机是UNCONNECTED状态,没有与LOGOUT事件关联的状态转移,故不操作。
  • 使用spring实现的状态机将类之间的关系全部交由了IOC容器做管理,实现了真正意义上的解耦。果然Spring大法好啊。


Spring StateMachine 让状态机结构更加层次化,我们来回顾下几个核心步骤:


  • 第一步,定义状态枚举。
  • 第二步,定义事件枚举。
  • 第三步,定义状态机配置,设置初始状态,以及状态与事件之间的关系。
  • 第四步,定义状态监听器,当状态变更时,触发方法。

状态转移的监听器


状态转移过程中,可以通过监听器(Listener)来处理一些持久化或者业务监控等任务。在需要持久化的场景中,可以在状态机模式中的监听器中添加持久化的处理。


其中主要涉及到

StateMachineListener事件监听器(通过Spring的event机制实现)。


  • 监听stateEntered(进入状态)、stateExited(离开状态)、eventNotAccepted(事件无法响应)、transition(转换)、transitionStarted(转换开始)、transitionEnded(转换结束)、stateMachineStarted(状态机启动)、stateMachineStopped(状态机关闭)、stateMachineError(状态机异常)等事件,借助listener可以跟踪状态转移。
  • StateChangeInterceptor拦截器接口,不同于Listener。其可以改变状态转移链的变化。主要在preEvent(事件预处理)、preStateChange(状态变更的前置处理)、postStateChange(状态变更的后置处理)、preTransition(转化的前置处理)、postTransition(转化的后置处理)、stateMachineError(异常处理)等执行点生效。
  • StateMachine 状态机实例,spring statemachine支持单例、工厂模式两种方式创建,每个statemachine有一个独有的machineId用于标识machine实例;需要注意的是statemachine实例内部存储了当前状态机等上下文相关的属性,因此这个实例不能够被多线程共享。



为了方便扩展更多的Listener,以及管理Listeners和Interceptors。可以定义一个基于状态机实例的Handler: PersistStateMachineHandler,以及持久化实体的监听器OrderPersistStateChangeListener如下:


监听器的Handler以及接口定义PersistStateMachineHandler:

public class PersistStateMachineHandler extends LifecycleObjectSupport {
   private final StateMachine<OrderStatus, OrderStatusChangeEvent> stateMachine;
   private final PersistingStateChangeInterceptor interceptor = new 
         PersistingStateChangeInterceptor();
   private final CompositePersistStateChangeListener listeners = new 
         CompositePersistStateChangeListener();
   /**
    * 实例化一个新的持久化状态机Handler
    *
    * @param stateMachine 状态机实例
    */
   public PersistStateMachineHandler(StateMachine<OrderStatus, OrderStatusChangeEvent> 
       stateMachine) {
       Assert.notNull(stateMachine, "State machine must be set");
       this.stateMachine = stateMachine;
   }
   @Override
   protected void onInit() throws Exception {
       stateMachine.getStateMachineAccessor().doWithAllRegions(function -> 
       function.addStateMachineInterceptor(interceptor));
   }
   /**
    * 处理entity的事件
    *
    * @param event
    * @param state
    * @return 如果事件被接受处理,返回true
    */
   public boolean handleEventWithState(Message<OrderStatusChangeEvent> event, OrderStatus 
       state) {
       stateMachine.stop();
       List<StateMachineAccess<OrderStatus, OrderStatusChangeEvent>> withAllRegions = 
        stateMachine.getStateMachineAccessor()
               .withAllRegions();
       for (StateMachineAccess<OrderStatus, OrderStatusChangeEvent> a : withAllRegions) {
           a.resetStateMachine(new DefaultStateMachineContext<>(state, null, null, null));
       }
       stateMachine.start();
       return stateMachine.sendEvent(event);
   }
   /**
    * 添加listener
    *
    * @param listener the listener
    */
   public void addPersistStateChangeListener(PersistStateChangeListener listener) {
       listeners.register(listener);
   }
   /**
    * 可以通过 addPersistStateChangeListener,增加当前Handler的PersistStateChangeListener。
    * 在状态变化的持久化触发时,会调用相应的实现了PersistStateChangeListener的Listener实例。
    */
   public interface PersistStateChangeListener {
       /**
        * 当状态被持久化,调用此方法
        *
        * @param state
        * @param message
        * @param transition
        * @param stateMachine 状态机实例
        */
       void onPersist(State<OrderStatus, OrderStatusChangeEvent> state, 
           Message<OrderStatusChangeEvent> message, Transition<OrderStatus,
               OrderStatusChangeEvent> transition,
                      StateMachine<OrderStatus, OrderStatusChangeEvent> stateMachine);
   }
private class PersistingStateChangeInterceptor extends   
         StateMachineInterceptorAdapter<OrderStatus, OrderStatusChangeEvent> {
       // 状态预处理的拦截器方法
       @Override
       public void preStateChange(State<OrderStatus, OrderStatusChangeEvent> state, 
             Message<OrderStatusChangeEvent> message,
                                  Transition<OrderStatus, OrderStatusChangeEvent> transition, 
               StateMachine<OrderStatus,
               OrderStatusChangeEvent> stateMachine) {
           listeners.onPersist(state, message, transition, stateMachine);
       }
   }
   private class CompositePersistStateChangeListener extends 
           AbstractCompositeListener<PersistStateChangeListener> implements
           PersistStateChangeListener {
       @Override
       public void onPersist(State<OrderStatus, OrderStatusChangeEvent> state, 
               Message<OrderStatusChangeEvent> message,
                             Transition<OrderStatus, OrderStatusChangeEvent> transition,     
             StateMachine<OrderStatus,
               OrderStatusChangeEvent> stateMachine) {
           for (Iterator<PersistStateChangeListener> iterator = getListeners().reverse(); 
               iterator.hasNext(); ) {
               PersistStateChangeListener listener = iterator.next();
               listener.onPersist(state, message, transition, stateMachine);
           }
       }
   }
}
复制代码



持久化状态发生变化的订单实体的Listener实现类OrderPersistStateChangeListener:

public class OrderPersistStateChangeListener implements 
                  PersistStateMachineHandler.PersistStateChangeListener {
    @Autowired
    private OrderRepo repo;
    @Override
   public void onPersist(State<OrderStatus, OrderStatusChangeEvent> state,   
        Message<OrderStatusChangeEvent> message,
                          Transition<OrderStatus, OrderStatusChangeEvent> transition, 
      StateMachine<OrderStatus, OrderStatusChangeEvent> stateMachine) {
        if (message != null && message.getHeaders().containsKey("order")) {
            Integer order = message.getHeaders().get("order", Integer.class);
            Order o = repo.findByOrderId(order);
            OrderStatus status = state.getId();
            o.setStatus(status);
            repo.save(o);
        }
    }
}
复制代码



Springboot注入Handler和Listener bean的Configuration类,OrderPersistHandlerConfig

@Configuration
public class OrderPersistHandlerConfig {
    @Autowired
    private StateMachine<OrderStatus, OrderStatusChangeEvent> stateMachine;
    @Bean
    public OrderStateService persist() {
        PersistStateMachineHandler handler = persistStateMachineHandler();
        handler.addPersistStateChangeListener(persistStateChangeListener());
        return new OrderStateService(handler);
    }
    @Bean
    public PersistStateMachineHandler persistStateMachineHandler() {
        return new PersistStateMachineHandler(stateMachine);
    }
    @Bean
    public OrderPersistStateChangeListener persistStateChangeListener(){
        return new OrderPersistStateChangeListener();
    }
}
复制代码



订单服务的Controller&Service示例


示例提供了两个简单的接口,一个是查看所有订单列表,一个是改变一个订单的状态。


Controller如下OrderController:

@RestController
@RequestMapping("/orders")
public class OrderController {
    @Autowired
    private OrderStateService orderStateService;
    /**
     * 列出所有的订单列表
     *
     * @return
     */
    @RequestMapping(method = {RequestMethod.GET})
    public ResponseEntity orders() {
        String orders = orderStateService.listDbEntries();
        return new ResponseEntity(orders, HttpStatus.OK);
    }
    /**
     * 通过触发一个事件,改变一个订单的状态
     * @param orderId
     * @param event
     * @return
     */
    @RequestMapping(value = "/{orderId}", method = {RequestMethod.POST})
    public ResponseEntity processOrderState(@PathVariable("orderId") Integer orderId, @RequestParam("event") OrderStatusChangeEvent event) {
        Boolean result = orderStateService.change(orderId, event);
        return new ResponseEntity(result, HttpStatus.OK);
    }
}
复制代码



订单服务类OrderStateService:

@Component
public class OrderStateService {
    private PersistStateMachineHandler handler;
    public OrderStateService(PersistStateMachineHandler handler) {
        this.handler = handler;
    }
    @Autowired
    private OrderRepo repo;
    public String listDbEntries() {
        List<Order> orders = repo.findAll();
        StringJoiner sj = new StringJoiner(",");
        for (Order order : orders) {
            sj.add(order.toString());
        }
        return sj.toString();
    }
    public boolean change(int order, OrderStatusChangeEvent event) {
        Order o = repo.findByOrderId(order);
        return handler.handleEventWithState(MessageBuilder.withPayload(event).setHeader("order", order).build(), o.getStatus());
    }
}







相关文章
|
2月前
|
开发框架 负载均衡 Java
当热门技术负载均衡遇上 Spring Boot,开发者的梦想与挑战在此碰撞,你准备好了吗?
【8月更文挑战第29天】在互联网应用开发中,负载均衡至关重要,可避免单服务器过载导致性能下降或崩溃。Spring Boot 作为流行框架,提供了强大的负载均衡支持,通过合理分配请求至多台服务器,提升系统可用性与可靠性,优化资源利用。本文通过示例展示了如何在 Spring Boot 中配置负载均衡,包括添加依赖、创建负载均衡的 `RestTemplate` 实例及服务接口调用等步骤,帮助开发者构建高效、稳定的应用。随着业务扩展,掌握负载均衡技术将愈发关键。
49 6
|
6天前
|
存储 缓存 Java
在Spring Boot中使用缓存的技术解析
通过利用Spring Boot中的缓存支持,开发者可以轻松地实现高效和可扩展的缓存策略,进而提升应用的性能和用户体验。Spring Boot的声明式缓存抽象和对多种缓存技术的支持,使得集成和使用缓存变得前所未有的简单。无论是在开发新应用还是优化现有应用,合理地使用缓存都是提高性能的有效手段。
13 1
|
2月前
|
缓存 NoSQL Java
SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)
Spring Cache 是 Spring 提供的简易缓存方案,支持本地与 Redis 缓存。通过添加 `spring-boot-starter-data-redis` 和 `spring-boot-starter-cache` 依赖,并使用 `@EnableCaching` 开启缓存功能。JetCache 由阿里开源,功能更丰富,支持多级缓存和异步 API,通过引入 `jetcache-starter-redis` 依赖并配置 YAML 文件启用。Layering Cache 则提供分层缓存机制,需引入 `layering-cache-starter` 依赖并使用特定注解实现缓存逻辑。
349 1
SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)
|
2月前
|
NoSQL JavaScript 前端开发
SpringBoot+Vue实现校园二手系统。前后端分离技术【完整功能介绍+实现详情+源码】
文章介绍了如何使用SpringBoot和Vue实现一个校园二手系统,采用前后端分离技术。系统具备完整的功能,包括客户端和管理员端的界面设计、个人信息管理、商品浏览和交易、订单处理、公告发布等。技术栈包括Vue框架、ElementUI、SpringBoot、Mybatis-plus和Redis。文章还提供了部分源代码,展示了前后端的请求接口和Redis验证码功能实现,以及系统重构和模块化设计的一些思考。
SpringBoot+Vue实现校园二手系统。前后端分离技术【完整功能介绍+实现详情+源码】
|
2月前
|
Java 数据库连接 数据库
告别繁琐 SQL!Hibernate 入门指南带你轻松玩转 ORM,解锁高效数据库操作新姿势
【8月更文挑战第31天】Hibernate 是一款流行的 Java 持久层框架,简化了对象关系映射(ORM)过程,使开发者能以面向对象的方式进行数据持久化操作而无需直接编写 SQL 语句。本文提供 Hibernate 入门指南,介绍核心概念及示例代码,涵盖依赖引入、配置文件设置、实体类定义、工具类构建及基本 CRUD 操作。通过学习,你将掌握使用 Hibernate 简化数据持久化的技巧,为实际项目应用打下基础。
66 0
|
2月前
|
Java 前端开发 Spring
技术融合新潮流!Vaadin携手Spring Boot、React、Angular,引领Web开发变革,你准备好了吗?
【8月更文挑战第31天】本文探讨了Vaadin与Spring Boot、React及Angular等主流技术栈的最佳融合实践。Vaadin作为现代Java Web框架,与其他技术栈结合能更好地满足复杂应用需求。文中通过示例代码展示了如何在Spring Boot项目中集成Vaadin,以及如何在Vaadin项目中使用React和Angular组件,充分发挥各技术栈的优势,提升开发效率和用户体验。开发者可根据具体需求选择合适的技术组合。
35 0
|
2月前
|
缓存 Java 数据库连接
Spring Boot 资源文件属性配置,紧跟技术热点,为你的应用注入灵动活力!
【8月更文挑战第29天】在Spring Boot开发中,资源文件属性配置至关重要,它让开发者能灵活定制应用行为而不改动代码,极大提升了可维护性和扩展性。Spring Boot支持多种配置文件类型,如`application.properties`和`application.yml`,分别位于项目的resources目录下。`.properties`文件采用键值对形式,而`yml`文件则具有更清晰的层次结构,适合复杂配置。此外,Spring Boot还支持占位符引用和其他外部来源的属性值,便于不同环境下覆盖默认配置。通过合理配置,应用能快速适应各种环境与需求变化。
35 0
|
2月前
|
XML Java 决策智能
拥抱智能决策新纪元!Spring Boot携手LiteFlow规则引擎,让复杂业务处理如丝般顺滑,引领技术潮流!
【8月更文挑战第29天】LiteFlow是一款专为Java应用设计的轻量级规则引擎,支持条件、循环、分支等多种规则类型,具有组件化设计和高度可扩展性。通过自定义规则和事件监听,它可以显著提升代码的可维护性和可重用性。本文将详细介绍如何在Spring Boot项目中整合LiteFlow,并通过实际案例演示其强大功能。主要步骤包括:添加依赖、配置参数、定义组件及流程,并通过API触发执行。借助LiteFlow,复杂业务逻辑处理变得更加灵活高效。
54 0
|
2月前
|
Java BI API
SpringBoot + POI-TL:高效生成Word报表的技术盛宴
【8月更文挑战第22天】在日常的工作与学习中,文档处理特别是Word报表的自动生成,是许多项目中不可或缺的一环。传统的手工编辑Word文档不仅效率低下,还容易出错,特别是在需要批量生成包含动态数据的报告时,更是令人头疼不已。幸运的是,结合Spring Boot的简洁高效与POI-TL的强大功能,我们能够轻松实现Word报表的快速生成,既提升了工作效率,又保证了数据的准确性和报表的美观性。
175 0
|
2月前
|
存储 Java API
SpringBoot + MinIO 实现文件切片极速上传技术
【8月更文挑战第19天】在现代互联网应用中,文件上传是一个常见且重要的功能。然而,随着文件体积的增大,传统的文件上传方式往往面临效率低下、耗时过长等问题。为了提升大文件上传的速度和效率,我们可以采用文件切片上传技术,并结合SpringBoot和MinIO来实现这一功能。
151 0
下一篇
无影云桌面