fish 11 months ago
parent
commit
15f319dadf

+ 25 - 0
.dockerignore

@@ -0,0 +1,25 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/bin
+**/charts
+**/docker-compose*
+**/compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md

+ 3 - 0
.gitignore

@@ -2,3 +2,6 @@
 target/
 *.iml
 
+*.log
+docs/.vitepress/cache/
+docs/node_modules/

+ 28 - 0
docs/.vitepress/config.mts

@@ -0,0 +1,28 @@
+import { defineConfig } from 'vitepress'
+
+// https://vitepress.dev/reference/site-config
+export default defineConfig({
+  title: "Springboot-note",
+  description: "springboot 开发教程",
+  themeConfig: {
+    // https://vitepress.dev/reference/default-theme-config
+    nav: [
+      // { text: 'Home', link: '/' },
+      { text: '文档', link: '/base/quick_start' }
+    ],
+
+    sidebar: [
+      {
+        text: 'Springboot',
+        items: [
+          { text: 'Markdown Examples', link: '/markdown-examples' },
+          { text: 'Runtime API Examples', link: '/api-examples' }
+        ]
+      }
+    ],
+
+    socialLinks: [
+      // { icon: 'github', link: 'https://github.com/vuejs/vitepress' }
+    ]
+  }
+})

+ 0 - 57
docs/README.md

@@ -242,63 +242,6 @@ pom.xml 配置
 
 ```
 
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
 # spring boot 介绍
 
 ## 微服务

BIN
docs/base/assets/l-1706883377068.png


BIN
docs/base/assets/l-1706883377077.png


BIN
docs/base/assets/l-1706883423261.png


BIN
docs/base/assets/l-1706898716302.png


BIN
docs/base/assets/l.jpg


BIN
docs/base/assets/l.png


+ 119 - 0
docs/base/database.md

@@ -0,0 +1,119 @@
+# 持久层
+
+model层:
+```
+@Entity
+public class Book {
+ 
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    private long id;
+
+    @Column(nullable = false, unique = true)
+    private String title;
+
+    @Column(nullable = false)
+    private String author;
+}
+```
+
+model对应的Repository层:
+```
+public interface BookRepository extends CrudRepository<Book, Long> {
+    List<Book> findByTitle(String title);
+}
+```
+
+配置:
+```
+@EnableJpaRepositories("com.baeldung.persistence.repo") // Repository 接口所在包
+@EntityScan("com.baeldung.persistence.model") // 实体类所在包
+@SpringBootApplication 
+public class Application {
+```
+
+
+```
+spring.datasource.driver-class-name=org.h2.Driver
+spring.datasource.url=jdbc:h2:mem:bootapp;DB_CLOSE_DELAY=-1
+spring.datasource.username=sa
+spring.datasource.password=
+```
+
+
+在controller层调用数据:
+```
+
+@RestController
+@RequestMapping("/api/books")
+public class BookController {
+
+    @Autowired
+    private BookRepository bookRepository;
+
+    @GetMapping
+    public Iterable findAll() {
+        return bookRepository.findAll();
+    }
+
+    @GetMapping("/title/{bookTitle}")
+    public List findByTitle(@PathVariable String bookTitle) {
+        return bookRepository.findByTitle(bookTitle);
+    }
+
+    @GetMapping("/{id}")
+    public Book findOne(@PathVariable Long id) {
+        return bookRepository.findById(id)
+          .orElseThrow(BookNotFoundException::new);
+    }
+
+    @PostMapping
+    @ResponseStatus(HttpStatus.CREATED)
+    public Book create(@RequestBody Book book) {
+        return bookRepository.save(book);
+    }
+
+    @DeleteMapping("/{id}")
+    public void delete(@PathVariable Long id) {
+        bookRepository.findById(id)
+          .orElseThrow(BookNotFoundException::new);
+        bookRepository.deleteById(id);
+    }
+
+    @PutMapping("/{id}")
+    public Book updateBook(@RequestBody Book book, @PathVariable Long id) {
+        if (book.getId() != id) {
+          throw new BookIdMismatchException();
+        }
+        bookRepository.findById(id)
+          .orElseThrow(BookNotFoundException::new);
+        return bookRepository.save(book);
+    }
+}
+
+```
+
+
+## 异常处理
+```
+@ControllerAdvice
+public class RestExceptionHandler extends ResponseEntityExceptionHandler {
+
+    @ExceptionHandler({ BookNotFoundException.class })
+    protected ResponseEntity<Object> handleNotFound(
+      Exception ex, WebRequest request) {
+        return handleExceptionInternal(ex, "Book not found", 
+          new HttpHeaders(), HttpStatus.NOT_FOUND, request);
+    }
+
+    @ExceptionHandler({ BookIdMismatchException.class, 
+      ConstraintViolationException.class, 
+      DataIntegrityViolationException.class })
+    public ResponseEntity<Object> handleBadRequest(
+      Exception ex, WebRequest request) {
+        return handleExceptionInternal(ex, ex.getLocalizedMessage(), 
+          new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
+    }
+}
+
+```

+ 588 - 0
docs/base/quick_start copy 2.md

@@ -0,0 +1,588 @@
+#### 集成Open API
+
+------
+
+[Open API](https://www.openapis.org/)是一个标准,它的主要作用是描述REST API,既可以作为文档给开发者阅读,又可以让机器根据这个文档自动生成客户端代码等。
+
+在Spring Boot应用中,假设我们编写了一堆REST API,如何添加Open API的支持?
+
+我们只需要在`pom.xml`中加入以下依赖:
+
+```
+org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.0
+```
+
+然后呢?没有然后了,直接启动应用,打开浏览器输入`http://localhost:8080/swagger-ui.html`:
+
+![swagger-ui](assets/l.png)
+
+立刻可以看到自动生成的API文档,这里列出了3个API,来自`api-controller`(因为定义在`ApiController`这个类中),点击某个API还可以交互,即输入API参数,点“Try it out”按钮,获得运行结果。
+
+## 是不是太方便了!
+
+因为我们引入`springdoc-openapi-ui`这个依赖后,它自动引入Swagger UI用来创建API文档。可以给API加入一些描述信息,例如:
+
+```
+@RestController
+@RequestMapping("/api")
+public class ApiController {
+    ...
+    @Operation(summary = "Get specific user object by it's id.")
+	@GetMapping("/users/{id}")
+	public User user(@Parameter(description = "id of the user.") @PathVariable("id") long id) {
+		return userService.getUserById(id);
+	}
+    ...
+}
+```
+
+`@Operation`可以对API进行描述,`@Parameter`可以对参数进行描述,它们的目的是用于生成API文档的描述信息。添加了描述的API文档如下:
+
+![api-description](assets/l.png)
+
+大多数情况下,不需要任何配置,我们就直接得到了一个运行时动态生成的可交互的API文档,该API文档总是和代码保持同步,大大简化了文档的编写工作。
+
+要自定义文档的样式、控制某些API显示等,请参考[springdoc文档](https://springdoc.org/)。
+
+### 配置反向代理
+
+如果在服务器上,用户访问的域名是`https://example.com`,但内部是通过类似Nginx这样的反向代理访问实际的Spring Boot应用,比如`http://localhost:8080`,这个时候,在页面`https://example.com/swagger-ui.html`上,显示的URL仍然是`http://localhost:8080`,这样一来,就无法直接在页面执行API,非常不方便。
+
+这是因为Spring Boot内置的Tomcat默认获取的服务器名称是`localhost`,端口是实际监听端口,而不是对外暴露的域名和`80`或`443`端口。要让Tomcat获取到对外暴露的域名等信息,必须在Nginx配置中传入必要的HTTP Header,常用的配置如下:
+
+```
+# Nginx配置
+server {
+    ...
+    location / {
+        proxy_pass http://localhost:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+    ...
+}
+```
+
+然后,在Spring Boot的`application.yml`中,加入如下配置:
+
+```
+server:
+  # 实际监听端口:
+  port: 8080
+  # 从反向代理读取相关的HTTP Header:
+  forward-headers-strategy: native
+```
+
+重启Spring Boot应用,即可在Swagger中显示正确的URL。
+
+
+
+
+
+
+
+
+#### 集成Artemis
+
+Last updated: 5/18/2019 18:28 / Reads: 111608
+
+------
+
+ActiveMQ Artemis是一个JMS服务器,在[集成JMS](https://www.liaoxuefeng.com/wiki/1252599548343744/1304266721460258)一节中我们已经详细讨论了如何在Spring中集成Artemis,本节我们讨论如何在Spring Boot中集成Artemis。
+
+我们还是以实际工程为例,创建一个`springboot-jms`工程,引入的依赖除了`spring-boot-starter-web`,`spring-boot-starter-jdbc`等以外,新增`spring-boot-starter-artemis`:
+
+```
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-artemis</artifactId>
+</dependency>
+```
+
+同样无需指定版本号。
+
+如何创建Artemis服务器我们已经在[集成JMS](https://www.liaoxuefeng.com/wiki/1252599548343744/1304266721460258)一节中详细讲述了,此处不再重复。创建Artemis服务器后,我们在`application.yml`中加入相关配置:
+
+```
+spring:
+  artemis:
+    # 指定连接外部Artemis服务器,而不是启动嵌入式服务:
+    mode: native
+    # 服务器地址和端口号:
+    host: 127.0.0.1
+    port: 61616
+    # 连接用户名和口令由创建Artemis服务器时指定:
+    user: admin
+    password: password
+```
+
+和Spring版本的JMS代码相比,使用Spring Boot集成JMS时,只要引入了`spring-boot-starter-artemis`,Spring Boot会自动创建JMS相关的`ConnectionFactory`、`JmsListenerContainerFactory`、`JmsTemplate`等,无需我们再手动配置了。
+
+发送消息时只需要引入`JmsTemplate`:
+
+```
+@Component
+public class MessagingService {
+    @Autowired
+    JmsTemplate jmsTemplate;
+
+    public void sendMailMessage() throws Exception {
+        String text = "...";
+        jmsTemplate.send("jms/queue/mail", new MessageCreator() {
+            public Message createMessage(Session session) throws JMSException {
+                return session.createTextMessage(text);
+            }
+        });
+    }
+}
+```
+
+接收消息时只需要标注`@JmsListener`:
+
+```
+@Component
+public class MailMessageListener {
+    final Logger logger = LoggerFactory.getLogger(getClass());
+
+    @JmsListener(destination = "jms/queue/mail", concurrency = "10")
+    public void onMailMessageReceived(Message message) throws Exception {
+        logger.info("received message: " + message);
+    }
+}
+```
+
+可见,应用程序收发消息的逻辑和Spring中使用JMS完全相同,只是通过Spring Boot,我们把工程简化到只需要设定Artemis相关配置。
+
+
+
+
+
+#### 集成RabbitMQ
+
+Last updated: 5/18/2019 18:07 / Reads: 339871
+
+------
+
+前面我们讲了ActiveMQ Artemis,它实现了JMS的消息服务协议。JMS是JavaEE的消息服务标准接口,但是,如果Java程序要和另一种语言编写的程序通过消息服务器进行通信,那么JMS就不太适合了。
+
+AMQP是一种使用广泛的独立于语言的消息协议,它的全称是Advanced Message Queuing Protocol,即高级消息队列协议,它定义了一种二进制格式的消息流,任何编程语言都可以实现该协议。实际上,Artemis也支持AMQP,但实际应用最广泛的AMQP服务器是使用[Erlang](https://www.erlang.org/)编写的[RabbitMQ](https://www.rabbitmq.com/)。
+
+### 安装RabbitMQ
+
+我们先从RabbitMQ的官网[下载](https://www.rabbitmq.com/download.html)并安装RabbitMQ,安装和启动RabbitMQ请参考官方文档。要验证启动是否成功,可以访问RabbitMQ的管理后台[http://localhost:15672](http://localhost:15672/),如能看到登录界面表示RabbitMQ启动成功:
+
+![rabbitmq-manage](assets/l.jpg)
+
+RabbitMQ后台管理的默认用户名和口令均为`guest`。
+
+### AMQP协议
+
+AMQP协议和前面我们介绍的JMS协议有所不同。在JMS中,有两种类型的消息通道:
+
+1.  点对点的Queue,即Producer发送消息到指定的Queue,接收方从Queue收取消息;
+2.  一对多的Topic,即Producer发送消息到指定的Topic,任意多个在线的接收方均可从Topic获得一份完整的消息副本。
+
+但是AMQP协议比JMS要复杂一点,它只有Queue,没有Topic,并且引入了Exchange的概念。当Producer想要发送消息的时候,它将消息发送给Exchange,由Exchange将消息根据各种规则投递到一个或多个Queue:
+
+```ascii
+                                      ┌───────┐
+                                 ┌───>│Queue-1│
+                  ┌──────────┐   │    └───────┘
+              ┌──>│Exchange-1│───┤
+┌──────────┐  │   └──────────┘   │    ┌───────┐
+│Producer-1│──┤                  ├───>│Queue-2│
+└──────────┘  │   ┌──────────┐   │    └───────┘
+              └──>│Exchange-2│───┤
+                  └──────────┘   │    ┌───────┐
+                                 └───>│Queue-3│
+                                      └───────┘
+```
+
+如果某个Exchange总是把消息发送到固定的Queue,那么这个消息通道就相当于JMS的Queue。如果某个Exchange把消息发送到多个Queue,那么这个消息通道就相当于JMS的Topic。和JMS的Topic相比,Exchange的投递规则更灵活,比如一个“登录成功”的消息被投递到Queue-1和Queue-2,而“登录失败”的消息则被投递到Queue-3。这些路由规则称之为Binding,通常都在RabbitMQ的管理后台设置。
+
+我们以具体的业务为例子,在RabbitMQ中,首先创建3个Queue,分别用于发送邮件、短信和App通知:
+
+![queues](assets/l.jpg)
+
+创建Queue时注意到可配置为持久化(Durable)和非持久化(Transient),当Consumer不在线时,持久化的Queue会暂存消息,非持久化的Queue会丢弃消息。
+
+紧接着,我们在Exchanges中创建一个Direct类型的Exchange,命名为`registration`,并添加如下两个Binding:
+
+![exchange-registration](assets/l-1706883377068.png)
+
+上述Binding的规则就是:凡是发送到`registration`这个Exchange的消息,均被发送到`q_mail`和`q_sms`这两个Queue。
+
+我们再创建一个Direct类型的Exchange,命名为`login`,并添加如下Binding:
+
+![exchange-login](assets/l-1706883377077.png)
+
+上述Binding的规则稍微复杂一点,当发送消息给`login`这个Exchange时,如果消息没有指定Routing Key,则被投递到`q_app`和`q_mail`,如果消息指定了Routing Key="login_failed",那么消息被投递到`q_sms`。
+
+配置好RabbitMQ后,我们就可以基于Spring Boot开发AMQP程序。
+
+### 使用RabbitMQ
+
+我们首先创建Spring Boot工程`springboot-rabbitmq`,并添加如下依赖引入RabbitMQ:
+
+```
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-amqp</artifactId>
+</dependency>
+```
+
+然后在`application.yml`中添加RabbitMQ相关配置:
+
+```
+spring:
+  rabbitmq:
+    host: localhost
+    port: 5672
+    username: guest
+    password: guest
+```
+
+我们还需要在`Application`中添加一个`MessageConverter`:
+
+```
+import org.springframework.amqp.support.converter.MessageConverter;
+
+@SpringBootApplication
+public class Application {
+    ...
+
+    @Bean
+    MessageConverter createMessageConverter() {
+        return new Jackson2JsonMessageConverter();
+    }
+}
+```
+
+`MessageConverter`用于将Java对象转换为RabbitMQ的消息。默认情况下,Spring Boot使用`SimpleMessageConverter`,只能发送`String`和`byte[]`类型的消息,不太方便。使用`Jackson2JsonMessageConverter`,我们就可以发送JavaBean对象,由Spring Boot自动序列化为JSON并以文本消息传递。
+
+因为引入了starter,所有RabbitMQ相关的Bean均自动装配,我们需要在Producer注入的是`RabbitTemplate`:
+
+```
+@Component
+public class MessagingService {
+    @Autowired
+    RabbitTemplate rabbitTemplate;
+
+    public void sendRegistrationMessage(RegistrationMessage msg) {
+        rabbitTemplate.convertAndSend("registration", "", msg);
+    }
+
+    public void sendLoginMessage(LoginMessage msg) {
+        String routingKey = msg.success ? "" : "login_failed";
+        rabbitTemplate.convertAndSend("login", routingKey, msg);
+    }
+}
+```
+
+发送消息时,使用`convertAndSend(exchange, routingKey, message)`可以指定Exchange、Routing Key以及消息本身。这里传入JavaBean后会自动序列化为JSON文本。上述代码将`RegistrationMessage`发送到`registration`,将`LoginMessage`发送到`login`,并根据登录是否成功来指定Routing Key。
+
+接收消息时,需要在消息处理的方法上标注`@RabbitListener`:
+
+```
+@Component
+public class QueueMessageListener {
+    final Logger logger = LoggerFactory.getLogger(getClass());
+
+    static final String QUEUE_MAIL = "q_mail";
+    static final String QUEUE_SMS = "q_sms";
+    static final String QUEUE_APP = "q_app";
+
+    @RabbitListener(queues = QUEUE_MAIL)
+    public void onRegistrationMessageFromMailQueue(RegistrationMessage message) throws Exception {
+        logger.info("queue {} received registration message: {}", QUEUE_MAIL, message);
+    }
+
+    @RabbitListener(queues = QUEUE_SMS)
+    public void onRegistrationMessageFromSmsQueue(RegistrationMessage message) throws Exception {
+        logger.info("queue {} received registration message: {}", QUEUE_SMS, message);
+    }
+
+    @RabbitListener(queues = QUEUE_MAIL)
+    public void onLoginMessageFromMailQueue(LoginMessage message) throws Exception {
+        logger.info("queue {} received message: {}", QUEUE_MAIL, message);
+    }
+
+    @RabbitListener(queues = QUEUE_SMS)
+    public void onLoginMessageFromSmsQueue(LoginMessage message) throws Exception {
+        logger.info("queue {} received message: {}", QUEUE_SMS, message);
+    }
+
+    @RabbitListener(queues = QUEUE_APP)
+    public void onLoginMessageFromAppQueue(LoginMessage message) throws Exception {
+        logger.info("queue {} received message: {}", QUEUE_APP, message);
+    }
+}
+```
+
+上述代码一共定义了5个Consumer,监听3个Queue。
+
+启动应用程序,我们注册一个新用户,然后发送一条`RegistrationMessage`消息。此时,根据`registration`这个Exchange的设定,我们会在两个Queue收到消息:
+
+```
+... c.i.learnjava.service.UserService        : try register by bob@example.com...
+... c.i.learnjava.web.UserController         : user registered: bob@example.com
+... c.i.l.service.QueueMessageListener       : queue q_mail received registration message: [RegistrationMessage: email=bob@example.com, name=Bob, timestamp=1594559871495]
+... c.i.l.service.QueueMessageListener       : queue q_sms received registration message: [RegistrationMessage: email=bob@example.com, name=Bob, timestamp=1594559871495]
+```
+
+当我们登录失败时,发送`LoginMessage`并设定Routing Key为`login_failed`,此时,只有`q_sms`会收到消息:
+
+```
+... c.i.learnjava.service.UserService        : try login by bob@example.com...
+... c.i.l.service.QueueMessageListener       : queue q_sms received message: [LoginMessage: email=bob@example.com, name=(unknown), success=false, timestamp=1594559886722]
+```
+
+登录成功后,发送`LoginMessage`,此时,`q_mail`和`q_app`将收到消息:
+
+```
+... c.i.learnjava.service.UserService        : try login by bob@example.com...
+... c.i.l.service.QueueMessageListener       : queue q_mail received message: [LoginMessage: email=bob@example.com, name=Bob, success=true, timestamp=1594559895251]
+... c.i.l.service.QueueMessageListener       : queue q_app received message: [LoginMessage: email=bob@example.com, name=Bob, success=true, timestamp=1594559895251]
+```
+
+RabbitMQ还提供了使用Topic的Exchange(此Topic指消息的标签,并非JMS的Topic概念),可以使用`*`进行匹配并路由。可见,掌握RabbitMQ的核心是理解其消息的路由规则。
+
+直接指定一个Queue并投递消息也是可以的,此时指定Routing Key为Queue的名称即可,因为RabbitMQ提供了一个`default exchange`用于根据Routing Key查找Queue并直接投递消息到指定的Queue。但是要实现一对多的投递就必须自己配置Exchange。
+
+### 练习
+
+
+
+
+
+
+
+#### 集成Kafka
+
+Last updated: 5/18/2019 18:27 / Reads: 299889
+
+------
+
+我们在前面已经介绍了JMS和AMQP,JMS是JavaEE的标准消息接口,Artemis是一个JMS实现产品,AMQP是跨语言的一个标准消息接口,RabbitMQ是一个AMQP实现产品。
+
+Kafka也是一个消息服务器,它的特点一是快,二是有巨大的吞吐量,那么Kafka实现了什么标准消息接口呢?
+
+Kafka没有实现任何标准的消息接口,它自己提供的API就是Kafka的接口。
+
+>   哥没有实现任何标准,哥自己就是标准。
+>
+>   —— Kafka
+
+Kafka本身是Scala编写的,运行在JVM之上。Producer和Consumer都通过Kafka的客户端使用网络来与之通信。从逻辑上讲,Kafka设计非常简单,它只有一种类似JMS的Topic的消息通道:
+
+```ascii
+                              ┌──────────┐
+                          ┌──>│Consumer-1│
+                          │   └──────────┘
+┌────────┐      ┌─────┐   │   ┌──────────┐
+│Producer│─────>│Topic│───┼──>│Consumer-2│
+└────────┘      └─────┘   │   └──────────┘
+                          │   ┌──────────┐
+                          └──>│Consumer-3│
+                              └──────────┘
+```
+
+那么Kafka如何支持十万甚至百万的并发呢?答案是分区。Kafka的一个Topic可以有一个至多个Partition,并且可以分布到多台机器上:
+
+```ascii
+            ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
+             Topic
+            │                   │
+                ┌───────────┐        ┌──────────┐
+            │┌─>│Partition-1│──┐│┌──>│Consumer-1│
+             │  └───────────┘  │ │   └──────────┘
+┌────────┐  ││  ┌───────────┐  │││   ┌──────────┐
+│Producer│───┼─>│Partition-2│──┼─┼──>│Consumer-2│
+└────────┘  ││  └───────────┘  │││   └──────────┘
+             │  ┌───────────┐  │ │   ┌──────────┐
+            │└─>│Partition-3│──┘│└──>│Consumer-3│
+                └───────────┘        └──────────┘
+            └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
+```
+
+Kafka只保证在一个Partition内部,消息是有序的,但是,存在多个Partition的情况下,Producer发送的3个消息会依次发送到Partition-1、Partition-2和Partition-3,Consumer从3个Partition接收的消息并不一定是Producer发送的顺序,因此,多个Partition只能保证接收消息大概率按发送时间有序,并不能保证完全按Producer发送的顺序。这一点在使用Kafka作为消息服务器时要特别注意,对发送顺序有严格要求的Topic只能有一个Partition。
+
+Kafka的另一个特点是消息发送和接收都尽量使用批处理,一次处理几十甚至上百条消息,比一次一条效率要高很多。
+
+最后要注意的是消息的持久性。Kafka总是将消息写入Partition对应的文件,消息保存多久取决于服务器的配置,可以按照时间删除(默认3天),也可以按照文件大小删除,因此,只要Consumer在离线期内的消息还没有被删除,再次上线仍然可以接收到完整的消息流。这一功能实际上是客户端自己实现的,客户端会存储它接收到的最后一个消息的offsetId,再次上线后按上次的offsetId查询。offsetId是Kafka标识某个Partion的每一条消息的递增整数,客户端通常将它存储在ZooKeeper中。
+
+有了Kafka消息设计的基本概念,我们来看看如何在Spring Boot中使用Kafka。
+
+### 安装Kafka
+
+首先从Kafka官网[下载](https://kafka.apache.org/downloads)最新版Kafaka,解压后在`bin`目录找到两个文件:
+
+-   `zookeeper-server-start.sh`:启动ZooKeeper(已内置在Kafka中);
+-   `kafka-server-start.sh`:启动Kafka。
+
+先启动ZooKeeper:
+
+```
+$ ./zookeeper-server-start.sh ../config/zookeeper.properties 
+```
+
+再启动Kafka:
+
+```
+./kafka-server-start.sh ../config/server.properties
+```
+
+看到如下输出表示启动成功:
+
+```
+... INFO [KafkaServer id=0] started (kafka.server.KafkaServer)
+```
+
+如果要关闭Kafka和ZooKeeper,依次按Ctrl-C退出即可。注意这是在本地开发时使用Kafka的方式,线上Kafka服务推荐使用云服务厂商托管模式(AWS的MSK,阿里云的消息队列Kafka版)。
+
+### 使用Kafka
+
+在Spring Boot中使用Kafka,首先要引入依赖:
+
+```
+<dependency>
+    <groupId>org.springframework.kafka</groupId>
+    <artifactId>spring-kafka</artifactId>
+</dependency>
+```
+
+注意这个依赖是`spring-kafka`项目提供的。
+
+然后,在`application.yml`中添加Kafka配置:
+
+```
+spring:
+  kafka:
+    bootstrap-servers: localhost:9092
+    consumer:
+      auto-offset-reset: latest
+      max-poll-records: 100
+      max-partition-fetch-bytes: 1000000
+```
+
+除了`bootstrap-servers`必须指定外,`consumer`相关的配置项均为调优选项。例如,`max-poll-records`表示一次最多抓取100条消息。配置名称去哪里看?IDE里定义一个`KafkaProperties.Consumer`的变量:
+
+```
+KafkaProperties.Consumer c = null;
+```
+
+然后按住Ctrl查看源码即可。
+
+### 发送消息
+
+Spring Boot自动为我们创建一个`KafkaTemplate`用于发送消息。注意到这是一个泛型类,而默认配置总是使用`String`作为Kafka消息的类型,所以注入`KafkaTemplate<String, String>`即可:
+
+```
+@Component
+public class MessagingService {
+    @Autowired ObjectMapper objectMapper;
+
+    @Autowired KafkaTemplate<String, String> kafkaTemplate;
+
+    public void sendRegistrationMessage(RegistrationMessage msg) throws IOException {
+        send("topic_registration", msg);
+    }
+
+    public void sendLoginMessage(LoginMessage msg) throws IOException {
+        send("topic_login", msg);
+    }
+
+    private void send(String topic, Object msg) throws IOException {
+        ProducerRecord<String, String> pr = new ProducerRecord<>(topic, objectMapper.writeValueAsString(msg));
+        pr.headers().add("type", msg.getClass().getName().getBytes(StandardCharsets.UTF_8));
+        kafkaTemplate.send(pr);
+    }
+}
+```
+
+发送消息时,需指定Topic名称,消息正文。为了发送一个JavaBean,这里我们没有使用`MessageConverter`来转换JavaBean,而是直接把消息类型作为Header添加到消息中,Header名称为`type`,值为Class全名。消息正文是序列化的JSON。
+
+### 接收消息
+
+接收消息可以使用`@KafkaListener`注解:
+
+```
+@Component
+public class TopicMessageListener {
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Autowired
+    ObjectMapper objectMapper;
+
+    @KafkaListener(topics = "topic_registration", groupId = "group1")
+    public void onRegistrationMessage(@Payload String message, @Header("type") String type) throws Exception {
+        RegistrationMessage msg = objectMapper.readValue(message, getType(type));
+        logger.info("received registration message: {}", msg);
+    }
+
+    @KafkaListener(topics = "topic_login", groupId = "group1")
+    public void onLoginMessage(@Payload String message, @Header("type") String type) throws Exception {
+        LoginMessage msg = objectMapper.readValue(message, getType(type));
+        logger.info("received login message: {}", msg);
+    }
+
+    @KafkaListener(topics = "topic_login", groupId = "group2")
+    public void processLoginMessage(@Payload String message, @Header("type") String type) throws Exception {
+        LoginMessage msg = objectMapper.readValue(message, getType(type));
+        logger.info("process login message: {}", msg);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> Class<T> getType(String type) {
+        // TODO: use cache:
+        try {
+            return (Class<T>) Class.forName(type);
+        } catch (ClassNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
+```
+
+在接收消息的方法中,使用`@Payload`表示传入的是消息正文,使用`@Header`可传入消息的指定Header,这里传入`@Header("type")`,就是我们发送消息时指定的Class全名。接收消息时,我们需要根据Class全名来反序列化获得JavaBean。
+
+上述代码一共定义了3个Listener,其中有两个方法监听的是同一个Topic,但它们的Group ID不同。假设Producer发送的消息流是A、B、C、D,Group ID不同表示这是两个不同的Consumer,它们将分别收取完整的消息流,即各自均收到A、B、C、D。Group ID相同的多个Consumer实际上被视作一个Consumer,即如果有两个Group ID相同的Consumer,那么它们各自收到的很可能是A、C和B、D。
+
+运行应用程序,注册新用户后,观察日志输出:
+
+```
+... c.i.learnjava.service.UserService        : try register by bob@example.com...
+... c.i.learnjava.web.UserController         : user registered: bob@example.com
+... c.i.l.service.TopicMessageListener       : received registration message: [RegistrationMessage: email=bob@example.com, name=Bob, timestamp=1594637517458]
+```
+
+用户登录后,观察日志输出:
+
+```
+... c.i.learnjava.service.UserService        : try login by bob@example.com...
+... c.i.l.service.TopicMessageListener       : received login message: [LoginMessage: email=bob@example.com, name=Bob, success=true, timestamp=1594637523470]
+... c.i.l.service.TopicMessageListener       : process login message: [LoginMessage: email=bob@example.com, name=Bob, success=true, timestamp=1594637523470]
+```
+
+因为Group ID不同,同一个消息被两个Consumer分别独立接收。如果把Group ID改为相同,那么同一个消息只会被两者之一接收。
+
+有细心的童鞋可能会问,在Kafka中是如何创建Topic的?又如何指定某个Topic的分区数量?
+
+实际上开发使用的Kafka默认允许自动创建Topic,创建Topic时默认的分区数量是2,可以通过`server.properties`修改默认分区数量。
+
+在生产环境中通常会关闭自动创建功能,Topic需要由运维人员先创建好。和RabbitMQ相比,Kafka并不提供网页版管理后台,管理Topic需要使用命令行,比较繁琐,只有云服务商通常会提供更友好的管理后台。
+
+### 练习
+
+
+
+
+
+
+
+
+
+
+

+ 19 - 0
docs/base/quick_start copy 3.md

@@ -0,0 +1,19 @@
+#### Spring Cloud开发
+
+Last updated: 2/18/2019 18:37 / Reads: 3505585
+
+------
+
+Spring是JavaEE的一个轻量级开发框架,主营IoC和AOP,集成JDBC、ORM、MVC等功能便于开发。
+
+Spring Boot是基于Spring,提供开箱即用的积木式组件,目的是提升开发效率。
+
+那么Spring Cloud是啥?
+
+Spring Cloud顾名思义是跟云相关的,云程序实际上就是指分布式应用程序,所以Spring Cloud就是为了让分布式应用程序编写更方便,更容易而提供的一组基础设施,它的核心是Spring框架,利用Spring Boot的自动配置,力图实现最简化的分布式应用程序开发。
+
+![springcloud](assets/l-1706883423261.png)
+
+Spring Cloud包含了一大堆技术组件,既有开源社区开发的组件,也有商业公司开发的组件,既有持续更新迭代的组件,也有即将退役不再维护的组件。
+
+本章会介绍如何基于Spring Cloud创建分布式应用程序,但并不会面面俱到地介绍所有组件,而是挑选几个核心组件,演示如何构造一个基本的分布式应用程序。

+ 0 - 0
docs/base/quick_start copy 4.md


+ 0 - 0
docs/base/quick_start copy 5.md


+ 63 - 0
docs/base/quick_start copy 6.md

@@ -0,0 +1,63 @@
+## 
+
+```
+
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-web</artifactId>
+</dependency>
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-data-jpa</artifactId>
+</dependency>
+<dependency>
+    <groupId>com.h2database</groupId>
+    <artifactId>h2</artifactId>
+</dependency>
+```
+
+
+视图模板:
+```
+<dependency> 
+    <groupId>org.springframework.boot</groupId> 
+    <artifactId>spring-boot-starter-thymeleaf</artifactId> 
+</dependency>
+```
+
+```
+
+spring.thymeleaf.cache=false
+spring.thymeleaf.enabled=true 
+spring.thymeleaf.prefix=classpath:/templates/
+spring.thymeleaf.suffix=.html
+
+spring.application.name=Bootstrap Spring Boot
+```
+
+安全:
+
+```
+<dependency> 
+    <groupId>org.springframework.boot</groupId> 
+    <artifactId>spring-boot-starter-security</artifactId> 
+</dependency>
+```
+
+所有端点都会根据 Spring Security 的内容协商策略使用 httpBasic 或 formLogin 进行默认安全保护
+```
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http.authorizeHttpRequests(expressionInterceptUrlRegistry ->
+                        expressionInterceptUrlRegistry
+                                .anyRequest()
+                                .permitAll())
+                .csrf(AbstractHttpConfigurer::disable);
+        return http.build();
+    }
+}
+```

File diff suppressed because it is too large
+ 641 - 0
docs/base/quick_start copy.md


+ 174 - 0
docs/base/quick_start.md

@@ -0,0 +1,174 @@
+# springboot-note
+
+springboot 学习项目,
+
+## 章节
+
+-   spring-boot-actuator
+    spring-boot-admin-simple
+    spring-boot-banner
+    spring-boot-commandLineRunner
+    spring-boot-docker
+    spring-boot-elasticsearch
+    spring-boot-fastDFS
+    spring-boot-file-upload
+    spring-boot-helloWorld
+    spring-boot-jpa
+    spring-boot-jpa-thymeleaf-curd
+    spring-boot-mail
+    spring-boot-memcache-spymemcached
+    spring-boot-mongodb
+    spring-boot-mybatis
+    spring-boot-mybatis-plus
+    spring-boot-package
+    spring-boot-package-war
+    spring-boot-rabbitmq
+    spring-boot-redis
+    spring-boot-scheduler
+    spring-boot-shiro
+    spring-boot-swagger
+    spring-boot-thymeleaf
+    spring-boot-web
+    spring-boot-web-thymeleaf
+    spring-boot-webflux
+
+
+## springboot 项目初始化
+
+
+Spring Boot Starter Parent 父级 pom 文件来统一管理版本依赖
+
+```
+<properties>
+    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    <java.version>1.8</java.version>
+    <resource.delimiter>@</resource.delimiter>
+    <maven.compiler.source>${java.version}</maven.compiler.source>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <maven.compiler.target>${java.version}</maven.compiler.target>
+</properties>
+```
+
+
+## spring-boot-devtools
+
+热部署,无需重复重启项目。
+
+```
+
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-devtools</artifactId>
+    <optional>true</optional>
+</dependency>
+```
+
+
+## Log4j2
+
+Spring Boot 中默认使用 Logback 作为日志框架,我们需要知道的是 Log4j2 是 Log4j 的升级版。添加依赖:
+
+```
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter</artifactId>
+    <exclusions>
+        <exclusion>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-logging</artifactId>
+        </exclusion>
+    </exclusions>
+</dependency>
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-log4j2</artifactId>
+</dependency>
+
+```
+
+log4j2.properties 配置文件:
+```
+status = error
+name = Log4j2Sample
+appenders = console
+
+appender.console.type = Console
+appender.console.name = STDOUT
+appender.console.layout.type = PatternLayout
+appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} - %msg%n
+
+rootLogger.level = warn
+rootLogger.appenderRefs = stdout
+rootLogger.appenderRef.stdout.ref = STDOUT
+```
+
+
+代码中打印日志:
+```
+		LOG.debug("debug 级别日志 ...");
+		LOG.info("info 级别日志 ...");
+		LOG.warn("warn 级别日志 ...");
+		LOG.error("error 级别日志 ...");
+		LOG.fatal("fatal 级别日志 ...");
+```
+
+
+## AOP
+
+通过自定义注解的方式,在 Spring Boot 中来实现 AOP 切面统一打印出入参日志
+
+```
+<!-- aop 依赖 -->
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-aop</artifactId>
+</dependency>
+
+<!-- 用于日志切面中,以 json 格式打印出入参 -->
+<dependency>
+    <groupId>com.google.code.gson</groupId>
+    <artifactId>gson</artifactId>
+    <version>2.8.5</version>
+</dependency>
+```
+
+
+
+## JPA
+
+## 自动配置
+
+AOP
+
+数据库
+
+mysql
+
+
+redis
+
+缓存: 文件,redis,EhCache
+
+
+
+mongo
+
+
+web,Restful,API,open api,
+
+Thymeleaf
+
+国际化
+
+注解,控制器,
+
+
+## Reference
+
+
+## License
+
+
+
+
+

+ 235 - 0
docs/base/redis.md

@@ -0,0 +1,235 @@
+#### 访问Redis
+
+------
+
+在Spring Boot中,要访问Redis,可以直接引入`spring-boot-starter-data-redis`依赖,它实际上是Spring Data的一个子项目——Spring Data Redis,主要用到了这几个组件:
+
+-   Lettuce:一个基于Netty的高性能Redis客户端;
+-   RedisTemplate:一个类似于JdbcTemplate的接口,用于简化Redis的操作。
+
+因为Spring Data Redis引入的依赖项很多,如果只是为了使用Redis,完全可以只引入Lettuce,剩下的操作都自己来完成。
+
+本节我们稍微深入一下Redis的客户端,看看怎么一步一步把一个第三方组件引入到Spring Boot中。
+
+首先,我们添加必要的几个依赖项:
+
+-   io.lettuce:lettuce-core
+-   org.apache.commons:commons-pool2
+
+注意我们并未指定版本号,因为在`spring-boot-starter-parent`中已经把常用组件的版本号确定下来了。
+
+第一步是在配置文件`application.yml`中添加Redis的相关配置:
+
+```
+spring:
+  redis:
+    host: ${REDIS_HOST:localhost}
+    port: ${REDIS_PORT:6379}
+    password: ${REDIS_PASSWORD:}
+    ssl: ${REDIS_SSL:false}
+    database: ${REDIS_DATABASE:0}
+```
+
+然后,通过`RedisConfiguration`来加载它:
+
+```
+@ConfigurationProperties("spring.redis")
+public class RedisConfiguration {
+	private String host;
+	private int port;
+	private String password;
+	private int database;
+
+    // getters and setters...
+}
+```
+
+再编写一个`@Bean`方法来创建`RedisClient`,可以直接放在`RedisConfiguration`中:
+
+```
+@ConfigurationProperties("spring.redis")
+public class RedisConfiguration {
+    ...
+
+    @Bean
+    RedisClient redisClient() {
+        RedisURI uri = RedisURI.Builder.redis(this.host, this.port)
+                .withPassword(this.password)
+                .withDatabase(this.database)
+                .build();
+        return RedisClient.create(uri);
+    }
+}
+```
+
+在启动入口引入该配置:
+
+```
+@SpringBootApplication
+@Import(RedisConfiguration.class) // 加载Redis配置
+public class Application {
+    ...
+}
+```
+
+注意:如果在`RedisConfiguration`中标注`@Configuration`,则可通过Spring Boot的自动扫描机制自动加载,否则,使用`@Import`手动加载。
+
+紧接着,我们用一个`RedisService`来封装所有的Redis操作。基础代码如下:
+
+```
+@Component
+public class RedisService {
+    @Autowired
+    RedisClient redisClient;
+
+    GenericObjectPool<StatefulRedisConnection<String, String>> redisConnectionPool;
+
+    @PostConstruct
+    public void init() {
+        GenericObjectPoolConfig<StatefulRedisConnection<String, String>> poolConfig = new GenericObjectPoolConfig<>();
+        poolConfig.setMaxTotal(20);
+        poolConfig.setMaxIdle(5);
+        poolConfig.setTestOnReturn(true);
+        poolConfig.setTestWhileIdle(true);
+        this.redisConnectionPool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connect(), poolConfig);
+    }
+
+    @PreDestroy
+    public void shutdown() {
+        this.redisConnectionPool.close();
+        this.redisClient.shutdown();
+    }
+}
+```
+
+注意到上述代码引入了Commons Pool的一个对象池,用于缓存Redis连接。因为Lettuce本身是基于Netty的异步驱动,在异步访问时并不需要创建连接池,但基于Servlet模型的同步访问时,连接池是有必要的。连接池在`@PostConstruct`方法中初始化,在`@PreDestroy`方法中关闭。
+
+下一步,是在`RedisService`中添加Redis访问方法。为了简化代码,我们仿照`JdbcTemplate.execute(ConnectionCallback)`方法,传入回调函数,可大幅减少样板代码。
+
+首先定义回调函数接口`SyncCommandCallback`:
+
+```
+@FunctionalInterface
+public interface SyncCommandCallback<T> {
+    // 在此操作Redis:
+    T doInConnection(RedisCommands<String, String> commands);
+}
+```
+
+编写`executeSync`方法,在该方法中,获取Redis连接,利用callback操作Redis,最后释放连接,并返回操作结果:
+
+```
+public <T> T executeSync(SyncCommandCallback<T> callback) {
+    try (StatefulRedisConnection<String, String> connection = redisConnectionPool.borrowObject()) {
+        connection.setAutoFlushCommands(true);
+        RedisCommands<String, String> commands = connection.sync();
+        return callback.doInConnection(commands);
+    } catch (Exception e) {
+        logger.warn("executeSync redis failed.", e);
+        throw new RuntimeException(e);
+    }
+}
+```
+
+有的童鞋觉得这样访问Redis的代码太复杂了,实际上我们可以针对常用操作把它封装一下,例如`set`和`get`命令:
+
+```
+public String set(String key, String value) {
+    return executeSync(commands -> commands.set(key, value));
+}
+
+public String get(String key) {
+    return executeSync(commands -> commands.get(key));
+}
+```
+
+类似的,`hget`和`hset`操作如下:
+
+```
+public boolean hset(String key, String field, String value) {
+    return executeSync(commands -> commands.hset(key, field, value));
+}
+
+public String hget(String key, String field) {
+    return executeSync(commands -> commands.hget(key, field));
+}
+
+public Map<String, String> hgetall(String key) {
+    return executeSync(commands -> commands.hgetall(key));
+}
+```
+
+常用命令可以提供方法接口,如果要执行任意复杂的操作,就可以通过`executeSync(SyncCommandCallback<T>)`来完成。
+
+完成了`RedisService`后,我们就可以使用Redis了。例如,在`UserController`中,我们在Session中只存放登录用户的ID,用户信息存放到Redis,提供两个方法用于读写:
+
+```
+@Controller
+public class UserController {
+    public static final String KEY_USER_ID = "__userid__";
+    public static final String KEY_USERS = "__users__";
+
+    @Autowired ObjectMapper objectMapper;
+    @Autowired RedisService redisService;
+
+    // 把User写入Redis:
+    private void putUserIntoRedis(User user) throws Exception {
+        redisService.hset(KEY_USERS, user.getId().toString(), objectMapper.writeValueAsString(user));
+    }
+
+    // 从Redis读取User:
+    private User getUserFromRedis(HttpSession session) throws Exception {
+        Long id = (Long) session.getAttribute(KEY_USER_ID);
+        if (id != null) {
+            String s = redisService.hget(KEY_USERS, id.toString());
+            if (s != null) {
+                return objectMapper.readValue(s, User.class);
+            }
+        }
+        return null;
+    }
+    ...
+}
+```
+
+用户登录成功后,把ID放入Session,把`User`实例放入Redis:
+
+```
+@PostMapping("/signin")
+public ModelAndView doSignin(@RequestParam("email") String email, @RequestParam("password") String password, HttpSession session) throws Exception {
+    try {
+        User user = userService.signin(email, password);
+        session.setAttribute(KEY_USER_ID, user.getId());
+        putUserIntoRedis(user);
+    } catch (RuntimeException e) {
+        return new ModelAndView("signin.html", Map.of("email", email, "error", "Signin failed"));
+    }
+    return new ModelAndView("redirect:/profile");
+}
+```
+
+需要获取`User`时,从Redis取出:
+
+```
+@GetMapping("/profile")
+public ModelAndView profile(HttpSession session) throws Exception {
+    User user = getUserFromRedis(session);
+    if (user == null) {
+        return new ModelAndView("redirect:/signin");
+    }
+    return new ModelAndView("profile.html", Map.of("user", user));
+}
+```
+
+从Redis读写Java对象时,序列化和反序列化是应用程序的工作,上述代码使用JSON作为序列化方案,简单可靠。也可将相关序列化操作封装到`RedisService`中,这样可以提供更加通用的方法:
+
+```
+public <T> T get(String key, Class<T> clazz) {
+    ...
+}
+
+public <T> T set(String key, T value) {
+    ...
+}
+```
+

+ 539 - 0
docs/base/spring_cloud.md

@@ -0,0 +1,539 @@
+#### Spring Cloud开发
+
+------
+
+Spring Cloud就是为了让分布式应用程序编写更方便。
+
+
+
+#### 搭建项目框架
+
+Last updated: 4/8/2023 11:12 / Reads: 345796
+
+------
+
+对于Warp Exchange项目,我们以Maven为构建工具,把每个模块作为一个Maven的项目管理,并抽取出公共逻辑放入`common`模块,结构如下:
+
+-   common:公共代码;
+-   config:配置服务器;
+-   push:推送服务;
+-   quotation:行情服务;
+-   trading-api:交易API服务;
+-   trading-engine:交易引擎;
+-   trading-sequencer:定序服务;
+-   ui:用户Web界面。
+
+为了简化版本和依赖管理,我们用`parent`模块管理最基础的`pom.xml`,其他模块直接从`parent`继承,能大大简化各自的`pom.xml`。`parent`模块`pom.xml`内容如下:
+
+```
+<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.itranswarp.exchange</groupId>
+    <artifactId>parent</artifactId>
+    <version>1.0</version>
+    <packaging>pom</packaging>
+
+    <!-- 继承自SpringBoot Starter Parent -->
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <!-- SpringBoot版本 -->
+        <version>3.0.0</version>
+    </parent>
+
+    <properties>
+        <!-- 项目版本 -->
+        <project.version>1.0</project.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+
+        <!-- Java编译和运行版本 -->
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <java.version>17</java.version>
+
+        <!-- 定义第三方组件的版本 -->
+        <pebble.version>3.2.0</pebble.version>
+        <springcloud.version>2022.0.0</springcloud.version>
+        <springdoc.version>2.0.0</springdoc.version>
+        <vertx.version>4.3.1</vertx.version>
+    </properties>
+
+    <!-- 引入SpringCloud依赖 -->
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${springcloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <!-- 共享的依赖管理 -->
+    <dependencies>
+        <!-- 依赖JUnit5 -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <!-- 依赖SpringTest -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <!-- 引入创建可执行Jar的插件 -->
+                <plugin>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-maven-plugin</artifactId>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+</project>
+```
+
+上述`pom.xml`中,除了写死的Spring Boot版本、Java运行版本、项目版本外,其他引入的版本均以`<xxx.version>1.23</xxx.version>`的形式定义,以便后续可以用`${xxx.version}`引用版本号,避免了同一个组件出现多个写死的版本定义。
+
+对其他业务模块,引入`parent`的`pom.xml`可大大简化配置。以`ui`模块为例,其`pom.xml`如下:
+
+```
+<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>
+
+    <!-- 指定Parent -->
+    <parent>
+        <groupId>com.itranswarp.exchange</groupId>
+        <artifactId>parent</artifactId>
+        <version>1.0</version>
+        <!-- Parent POM的相对路径 -->
+        <relativePath>../parent/pom.xml</relativePath>
+    </parent>
+
+    <!-- 当前模块名称 -->
+    <artifactId>ui</artifactId>
+
+    <dependencies>
+        <!-- 依赖SpringCloud Config客户端 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-config</artifactId>
+        </dependency>
+
+        <!-- 依赖SpringBoot Actuator -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+
+        <!-- 依赖Common模块 -->
+        <dependency>
+            <groupId>com.itranswarp.exchange</groupId>
+            <artifactId>common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <!-- 依赖第三方模块 -->
+        <dependency>
+            <groupId>io.pebbletemplates</groupId>
+            <artifactId>pebble-spring-boot-starter</artifactId>
+            <version>${pebble.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <!-- 指定输出文件名 -->
+        <finalName>${project.artifactId}</finalName>
+        <!-- 创建SpringBoot可执行jar -->
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>
+```
+
+因为我们在`parent`的`pom.xml`中引入了Spring Cloud的依赖管理,因此,无需指定相关组件的版本。只有我们自己编写的组件和未在Spring Boot和Spring Cloud中引入的组件,才需要指定版本。
+
+最后,我们还需要一个`build`模块,把所有模块放到一起编译。建立`build`文件夹并创建`pom.xml`如下:
+
+```
+<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/maven-v4_0_0.xsd"
+>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.itranswarp.exchange</groupId>
+    <artifactId>build</artifactId>
+    <version>1.0</version>
+    <packaging>pom</packaging>
+    <name>Warp Exchange</name>
+
+    <!-- 按相对路径列出所有模块 -->
+    <modules>
+        <module>../common</module>
+        <module>../config</module>
+        <module>../parent</module>
+        <module>../push</module>
+        <module>../quotation</module>
+        <module>../trading-api</module>
+        <module>../trading-engine</module>
+        <module>../trading-sequencer</module>
+        <module>../ui</module>
+    </modules>
+</project>
+```
+
+我们还需要创建目录`config-repo`来存储Spring Cloud Config服务器端的配置文件。
+
+最后,将所有模块导入IDE,可正常开发、编译、运行。如果要在命令行模式下运行,进入`build`文件夹使用Maven编译即可:
+
+```
+warpexchange $ cd build && mvn clean package
+```
+
+### 本地开发环境
+
+在本地开发时,我们需要经常调试代码。除了安装JDK,选择一个IDE外,我们还需要在本地运行MySQL、Redis、Kafka,以及Kafka依赖的ZooKeeper服务。
+
+考虑到手动安装各个服务在不同操作系统下的差异,以及初始化数据非常麻烦,我们使用[Docker Desktop](https://www.docker.com/products/docker-desktop/)来运行这些基础服务,需要在`build`目录下编写一个`docker-compose.yml`文件定义我们要运行的所有服务:
+
+```
+version: "3"
+services:
+  zookeeper:
+    image: bitnami/zookeeper:3.5
+    container_name: zookeeper
+    ports:
+      - "2181:2181"
+    environment:
+      - ALLOW_ANONYMOUS_LOGIN=yes
+    volumes:
+      - "./docker/zookeeper-data:/bitnami"
+
+  kafka:
+    image: bitnami/kafka:3.0
+    container_name: kafka
+    ports:
+      - "9092:9092"
+    depends_on:
+      - zookeeper
+    environment:
+      - KAFKA_BROKER_ID=1
+      - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092
+      - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092
+      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
+      - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true
+      - ALLOW_PLAINTEXT_LISTENER=yes
+    volumes:
+      - "./docker/kafka-data:/bitnami"
+
+  redis:
+    image: redis:6.2
+    container_name: redis
+    ports:
+      - "6379:6379"
+    volumes:
+      - "./docker/redis-data:/data"
+
+  mysql:
+    image: mysql:8
+    container_name: mysql
+    ports:
+      - "3306:3306"
+    command: --default-authentication-plugin=mysql_native_password
+    environment:
+      - MYSQL_ROOT_PASSWORD=password
+    volumes:
+      - "./sql/schema.sql:/docker-entrypoint-initdb.d/1-schema.sql:ro"
+      - "./docker/mysql-data:/var/lib/mysql"
+```
+
+在上述`docker-compose.yml`文件中,我们定义了MySQL、Redis、Kafka以及Kafka依赖的ZooKeeper服务,各服务均暴露标准端口,且MySQL的`root`口令设置为`password`,第一次启动MySQL时,使用`sql/schema.sql`文件初始化数据库表结构。所有数据盘均挂载到`build`目录下的`docker`目录。
+
+在`build`目录下运行`docker-compose up -d`即可启动容器:
+
+```
+build $ docker-compose up -d
+Creating network "build_default" with the default driver
+Creating zookeeper ... done
+Creating mysql     ... done
+Creating redis     ... done
+Creating kafka     ... done
+```
+
+在Docker Desktop中可看到运行状态:
+
+![docker-desktop](assets/l-1706898716302.png)
+
+如果要删除开发环境的所有数据,首先停止运行Docker容器进程并删除,然后删除`build`目录下的`docker`目录,重新运行`docker-compose`即可。
+
+### Spring Cloud Config
+
+Spring Cloud Config是Spring Cloud的一个子项目,它的主要目的是解决多个Spring Boot应用启动时,应该如何读取配置文件的问题。
+
+对于单体应用,即一个独立的Spring Boot应用,我们会把配置写在`application.yml`文件中。如果配置需要针对多个环境,可以用`---`分隔并标注好环境:
+
+```
+# application.yml
+# 通用配置:
+spring:
+  datasource:
+    url: jdbc:mysql://localhost/test
+
+---
+
+# test profile:
+spring:
+  config:
+    activate:
+      on-profile: test
+  datasource:
+    url: jdbc:mysql://172.16.0.100/test
+```
+
+这种配置方式针对单个Spring Boot应用是可行的,但是,针对分布式应用,有多个Spring Boot应用需要启动时,分散在各个应用中的配置既不便于管理,也不便于复用相同的配置。
+
+Spring Cloud Config提供了一个通用的分布式应用的配置解决方案。它把配置分为两部分:
+
+-   Config Server:配置服务器,负责读取所有配置;
+-   Config Client:嵌入到各个Spring Boot应用中,本地无配置信息,启动时向服务器请求配置。
+
+我们先来看看如何搭建一个Spring Cloud Config Server,即配置服务器。
+
+首先,在`config`模块中引入`spring-cloud-config-server`依赖:
+
+```
+<dependency>
+    <groupId>org.springframework.cloud</groupId>
+    <artifactId>spring-cloud-config-server</artifactId>
+</dependency>
+```
+
+然后,编写一个`ConfigApplication`入口,标注`@EnableConfigServer`:
+
+```
+@EnableConfigServer
+@SpringBootApplication
+public class ConfigApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(ConfigApplication.class, args);
+    }
+}
+```
+
+最后,在`application.yml`中设置如何搜索配置。Spring Cloud Config支持多种配置方式,包括从本地文件、Git仓库、数据库等多个地方读取配置。这里我们选择以本地文件的方式读取配置文件,这也是最简单的一种配置方式:
+
+```
+# 配置服务器的端口,通常设置为8888:
+server:
+  port: 8888
+
+spring:
+  application:
+    name: config-server
+  profiles:
+    # 从文件读取配置时,Config Server激活的profile必须设定为native:
+    active: native
+  cloud:
+    config:
+      server:
+        native:
+          # 设置配置文件的搜索路径:
+          search-locations: file:./config-repo, file:../config-repo, file:../../config-repo
+```
+
+在`config-repo`目录下,存放的就是一系列配置文件:
+
+```ascii
+config-repo/
+├── application-default.yml
+├── application-test.yml
+├── application.yml
+├── push.yml
+├── quotation.yml
+├── trading-api.yml
+├── trading-engine.yml
+├── trading-sequencer.yml
+├── ui-default.yml
+└── ui.yml
+```
+
+至此,配置服务器就完成了,直接运行`ConfigApplication`即可启动配置服务器。在开发过程中,保持配置服务器在后台运行即可。
+
+接下来,对于每个负责业务的Spring Boot应用,我们需要从Spring Cloud Config Server读取配置。读取配置并不是说本地零配置,还是需要一点基础配置信息。以`ui`项目为例,编写`application.yml`如下:
+
+```
+spring:
+  application:
+    # 设置app名称:
+    name: ui
+  config:
+    # 导入Config Server地址:
+    import: configserver:${CONFIG_SERVER:http://localhost:8888}
+```
+
+上述默认的Config Server配置为`http://localhost:8888`,也可以通过环境变量指定Config Server的地址。
+
+下一步是在`ui`模块的`pom.xml`中添加依赖:
+
+```
+<dependency>
+    <groupId>org.springframework.cloud</groupId>
+    <artifactId>spring-cloud-starter-config</artifactId>
+</dependency>
+```
+
+接下来正常启动`UIApplication`,该应用就会自动从Config Server读取配置。由于我们指定了应用的名称是`ui`,且默认的`profile`是`default`,因此,Config Server将返回以下4个配置文件:
+
+-   ui-default.yml
+-   application-default.yml
+-   ui.yml
+-   application.yml
+
+前面的配置文件优先级较高,后面的配置文件优先级较低。如果出现相同的配置项,则在优先级高的配置生效。
+
+我们可以在浏览器访问`http://localhost:8888/ui/default`看到Config Server返回的配置,它是一个JSON文件:
+
+```
+{
+    "name": "ui",
+    "profiles": [
+        "default"
+    ],
+    "label": null,
+    "version": null,
+    "state": null,
+    "propertySources": [
+        {
+            "name": "file:../config-repo/ui-default.yml",
+            "source": {...}
+        },
+        {
+            "name": "file:../config-repo/application-default.yml",
+            "source": {...}
+        },
+        {
+            "name": "file:../config-repo/ui.yml",
+            "source": {...}
+        },
+        {
+            "name": "file:../config-repo/application.yml",
+            "source": {...}
+        }
+    ]
+}
+```
+
+如果我们启动`UIApplication`时传入`SPRING_PROFILES_ACTIVE=test`,将profile设置为`test`,则Config Server返回的文件如下:
+
+-   ui-test.yml
+-   application-test.yml
+-   ui.yml
+-   application.yml
+
+可以通过`http://localhost:8888/ui/test`查看返回的配置。由于文件`ui-test.yml`不存在,因此,实际配置由3个文件合并而成。
+
+我们可以很容易地看到,一个Spring Boot应用在启动时,首先要设置自己的`name`并导入Config Server的URL,再根据当前活动的`profile`,由Config Server返回多个配置文件:
+
+-   {name}-{profile}.yml
+-   application-{profile}.yml
+-   {name}.yml
+-   application.yml
+
+其中,`{name}-{xxx}.yml`是针对某个应用+某个`profile`的特定配置,`{name}.yml`是针对某个应用+所有profile的配置,`application-{profile}.yml`是针对某个`profile`的全局配置,`application.yml`是所有应用的全局配置。搭配各种配置文件就可以灵活组合配置。一般来说,全局默认的配置放在`application.yml`中,例如数据库连接:
+
+```
+spring:
+  datasource:
+    url: jdbc:mysql://localhost/test
+```
+
+这样保证了默认连接到本地数据库,在生产环境中会直接报错而不是连接到错误的数据库。
+
+在生产环境,例如`profile`设置为`prod`,则可以将数据库连接写在`application-prod.yml`中,使得所有生产环境的应用读取到的数据库连接是一致的:
+
+```
+spring:
+  datasource:
+    url: jdbc:mysql://172.16.0.100/prod_db
+```
+
+某个应用自己特定的配置则应当放到`{name}.yml`和`{name}-{profile}.yml`中。
+
+在设置好各个配置文件后,应当通过浏览器检查Config Server返回的配置是否符合预期。
+
+Spring Cloud Config还支持配置多个profile,以及从加密的配置源读取配置等。如果遇到更复杂的需求,可参考[Spring Cloud Config的文档](https://spring.io/projects/spring-cloud-config#learn)。
+
+### 环境变量
+
+需要特别注意,在`config-repo`的配置文件里,使用的环境变量,不是Config Server的环境变量,而是具体某个Spring Boot应用的环境变量。
+
+我们举个例子:假定`ui.yml`定义如下:
+
+```
+server:
+  port: ${APP_PORT:8000}
+```
+
+当`UIApplication`启动时,它获得的配置为`server.port=${APP_PORT:8000}`。Config Server*不会替换任何环境变量*,而是将它们原封不动地返回给`UIApplication`,由`UIApplication`根据自己的环境变量解析后获得最终配置。如果我们启动`UIApplication`时传入环境变量:
+
+```
+$ java -DAPP_PORT=7000 -jar ui.jar
+```
+
+则`UIApplication`最终读取的配置`server.port`为`7000`。
+
+可见,使用Spring Cloud Config时,读取配置文件步骤如下:
+
+1.  启动XxxApplication时,读取自身的`application.yml`,获得`name`和Config Server地址;
+2.  根据`name`、`profile`和Config Server地址,获得一个或多个有优先级的配置文件;
+3.  按优先级合并配置项;
+4.  如果配置项中存在环境变量,则使用Xxx应用本身的环境变量去替换占位符。
+
+环境变量通常用于配置一些敏感信息,如数据库连接口令,它们不适合明文写在`config-repo`的配置文件里。
+
+### 常见错误
+
+启动一个Spring Boot应用时,如果出现`Unable to load config data`错误:
+
+```
+java.lang.IllegalStateException: Unable to load config data from 'configserver:http://localhost:8888'
+	at org.springframework.boot.context.config.StandardConfigDataLocationResolver.getReferences
+    at ...
+```
+
+需要检查是否在`pom.xml`中引入了`spring-cloud-starter-config`,因为没有引入该依赖时,应用无法解析本地配置的`import: configserver:xxx`。
+
+如果在启动一个Spring Boot应用时,Config Server没有运行,通常错误信息是因为没有读取到配置导致无法创建某个Bean。
+

+ 8 - 0
docs/base/spring_web.md

@@ -0,0 +1,8 @@
+## MVC
+
+定义MVC配置类:
+
+```
+public class MvcConfig implements WebMvcConfigurer {
+
+```

+ 37 - 0
docs/index.md

@@ -0,0 +1,37 @@
+---
+# https://vitepress.dev/reference/default-theme-home-page
+layout: home
+
+hero:
+  name: "springboot 教程"
+  text: "包含文档和源码"
+  tagline: 多个项目源码,实战,项目模板
+  actions:
+    # - theme: brand
+    #   text: Markdown Examples
+    #   link: /markdown-examples
+    - theme: alt
+      text: 查看文档
+      link: /base/quick_start
+
+# features:
+#   - title: Feature A
+#     details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
+#   - title: Feature B
+#     details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
+#   - title: Feature C
+#     details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
+---
+
+
+
+
+
+	<repositories>
+		<repository>
+			<id>nexus-aliyun</id>
+			<name>Nexus aliyun</name>
+			<url>http://maven.aliyun.com/nexus/content/groups/public</url>
+		</repository>
+	</repositories>
+

+ 1520 - 0
docs/package-lock.json

@@ -0,0 +1,1520 @@
+{
+  "name": "docs",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "devDependencies": {
+        "vitepress": "^1.0.0-rc.41"
+      }
+    },
+    "node_modules/@algolia/autocomplete-core": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz",
+      "integrity": "sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/autocomplete-plugin-algolia-insights": "1.9.3",
+        "@algolia/autocomplete-shared": "1.9.3"
+      }
+    },
+    "node_modules/@algolia/autocomplete-plugin-algolia-insights": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz",
+      "integrity": "sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/autocomplete-shared": "1.9.3"
+      },
+      "peerDependencies": {
+        "search-insights": ">= 1 < 3"
+      }
+    },
+    "node_modules/@algolia/autocomplete-preset-algolia": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz",
+      "integrity": "sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/autocomplete-shared": "1.9.3"
+      },
+      "peerDependencies": {
+        "@algolia/client-search": ">= 4.9.1 < 6",
+        "algoliasearch": ">= 4.9.1 < 6"
+      }
+    },
+    "node_modules/@algolia/autocomplete-shared": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz",
+      "integrity": "sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==",
+      "dev": true,
+      "peerDependencies": {
+        "@algolia/client-search": ">= 4.9.1 < 6",
+        "algoliasearch": ">= 4.9.1 < 6"
+      }
+    },
+    "node_modules/@algolia/cache-browser-local-storage": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.22.1.tgz",
+      "integrity": "sha512-Sw6IAmOCvvP6QNgY9j+Hv09mvkvEIDKjYW8ow0UDDAxSXy664RBNQk3i/0nt7gvceOJ6jGmOTimaZoY1THmU7g==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/cache-common": "4.22.1"
+      }
+    },
+    "node_modules/@algolia/cache-common": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/cache-common/-/cache-common-4.22.1.tgz",
+      "integrity": "sha512-TJMBKqZNKYB9TptRRjSUtevJeQVXRmg6rk9qgFKWvOy8jhCPdyNZV1nB3SKGufzvTVbomAukFR8guu/8NRKBTA==",
+      "dev": true
+    },
+    "node_modules/@algolia/cache-in-memory": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/cache-in-memory/-/cache-in-memory-4.22.1.tgz",
+      "integrity": "sha512-ve+6Ac2LhwpufuWavM/aHjLoNz/Z/sYSgNIXsinGofWOysPilQZPUetqLj8vbvi+DHZZaYSEP9H5SRVXnpsNNw==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/cache-common": "4.22.1"
+      }
+    },
+    "node_modules/@algolia/client-account": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/client-account/-/client-account-4.22.1.tgz",
+      "integrity": "sha512-k8m+oegM2zlns/TwZyi4YgCtyToackkOpE+xCaKCYfBfDtdGOaVZCM5YvGPtK+HGaJMIN/DoTL8asbM3NzHonw==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/client-common": "4.22.1",
+        "@algolia/client-search": "4.22.1",
+        "@algolia/transporter": "4.22.1"
+      }
+    },
+    "node_modules/@algolia/client-analytics": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/client-analytics/-/client-analytics-4.22.1.tgz",
+      "integrity": "sha512-1ssi9pyxyQNN4a7Ji9R50nSdISIumMFDwKNuwZipB6TkauJ8J7ha/uO60sPJFqQyqvvI+px7RSNRQT3Zrvzieg==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/client-common": "4.22.1",
+        "@algolia/client-search": "4.22.1",
+        "@algolia/requester-common": "4.22.1",
+        "@algolia/transporter": "4.22.1"
+      }
+    },
+    "node_modules/@algolia/client-common": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/client-common/-/client-common-4.22.1.tgz",
+      "integrity": "sha512-IvaL5v9mZtm4k4QHbBGDmU3wa/mKokmqNBqPj0K7lcR8ZDKzUorhcGp/u8PkPC/e0zoHSTvRh7TRkGX3Lm7iOQ==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/requester-common": "4.22.1",
+        "@algolia/transporter": "4.22.1"
+      }
+    },
+    "node_modules/@algolia/client-personalization": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/client-personalization/-/client-personalization-4.22.1.tgz",
+      "integrity": "sha512-sl+/klQJ93+4yaqZ7ezOttMQ/nczly/3GmgZXJ1xmoewP5jmdP/X/nV5U7EHHH3hCUEHeN7X1nsIhGPVt9E1cQ==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/client-common": "4.22.1",
+        "@algolia/requester-common": "4.22.1",
+        "@algolia/transporter": "4.22.1"
+      }
+    },
+    "node_modules/@algolia/client-search": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/client-search/-/client-search-4.22.1.tgz",
+      "integrity": "sha512-yb05NA4tNaOgx3+rOxAmFztgMTtGBi97X7PC3jyNeGiwkAjOZc2QrdZBYyIdcDLoI09N0gjtpClcackoTN0gPA==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/client-common": "4.22.1",
+        "@algolia/requester-common": "4.22.1",
+        "@algolia/transporter": "4.22.1"
+      }
+    },
+    "node_modules/@algolia/logger-common": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/logger-common/-/logger-common-4.22.1.tgz",
+      "integrity": "sha512-OnTFymd2odHSO39r4DSWRFETkBufnY2iGUZNrMXpIhF5cmFE8pGoINNPzwg02QLBlGSaLqdKy0bM8S0GyqPLBg==",
+      "dev": true
+    },
+    "node_modules/@algolia/logger-console": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/logger-console/-/logger-console-4.22.1.tgz",
+      "integrity": "sha512-O99rcqpVPKN1RlpgD6H3khUWylU24OXlzkavUAMy6QZd1776QAcauE3oP8CmD43nbaTjBexZj2nGsBH9Tc0FVA==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/logger-common": "4.22.1"
+      }
+    },
+    "node_modules/@algolia/requester-browser-xhr": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.22.1.tgz",
+      "integrity": "sha512-dtQGYIg6MteqT1Uay3J/0NDqD+UciHy3QgRbk7bNddOJu+p3hzjTRYESqEnoX/DpEkaNYdRHUKNylsqMpgwaEw==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/requester-common": "4.22.1"
+      }
+    },
+    "node_modules/@algolia/requester-common": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/requester-common/-/requester-common-4.22.1.tgz",
+      "integrity": "sha512-dgvhSAtg2MJnR+BxrIFqlLtkLlVVhas9HgYKMk2Uxiy5m6/8HZBL40JVAMb2LovoPFs9I/EWIoFVjOrFwzn5Qg==",
+      "dev": true
+    },
+    "node_modules/@algolia/requester-node-http": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/requester-node-http/-/requester-node-http-4.22.1.tgz",
+      "integrity": "sha512-JfmZ3MVFQkAU+zug8H3s8rZ6h0ahHZL/SpMaSasTCGYR5EEJsCc8SI5UZ6raPN2tjxa5bxS13BRpGSBUens7EA==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/requester-common": "4.22.1"
+      }
+    },
+    "node_modules/@algolia/transporter": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@algolia/transporter/-/transporter-4.22.1.tgz",
+      "integrity": "sha512-kzWgc2c9IdxMa3YqA6TN0NW5VrKYYW/BELIn7vnLyn+U/RFdZ4lxxt9/8yq3DKV5snvoDzzO4ClyejZRdV3lMQ==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/cache-common": "4.22.1",
+        "@algolia/logger-common": "4.22.1",
+        "@algolia/requester-common": "4.22.1"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.23.9",
+      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.23.9.tgz",
+      "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
+      "dev": true,
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@docsearch/css": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmmirror.com/@docsearch/css/-/css-3.5.2.tgz",
+      "integrity": "sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==",
+      "dev": true
+    },
+    "node_modules/@docsearch/js": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmmirror.com/@docsearch/js/-/js-3.5.2.tgz",
+      "integrity": "sha512-p1YFTCDflk8ieHgFJYfmyHBki1D61+U9idwrLh+GQQMrBSP3DLGKpy0XUJtPjAOPltcVbqsTjiPFfH7JImjUNg==",
+      "dev": true,
+      "dependencies": {
+        "@docsearch/react": "3.5.2",
+        "preact": "^10.0.0"
+      }
+    },
+    "node_modules/@docsearch/react": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmmirror.com/@docsearch/react/-/react-3.5.2.tgz",
+      "integrity": "sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/autocomplete-core": "1.9.3",
+        "@algolia/autocomplete-preset-algolia": "1.9.3",
+        "@docsearch/css": "3.5.2",
+        "algoliasearch": "^4.19.1"
+      },
+      "peerDependencies": {
+        "@types/react": ">= 16.8.0 < 19.0.0",
+        "react": ">= 16.8.0 < 19.0.0",
+        "react-dom": ">= 16.8.0 < 19.0.0",
+        "search-insights": ">= 1 < 3"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "react-dom": {
+          "optional": true
+        },
+        "search-insights": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
+      "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
+      "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
+      "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
+      "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
+      "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
+      "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
+      "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
+      "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
+      "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
+      "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
+      "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
+      "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
+      "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
+      "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
+      "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
+      "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
+      "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
+      "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
+      "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
+      "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
+      "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
+      "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
+      "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.4.15",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+      "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+      "dev": true
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz",
+      "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz",
+      "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz",
+      "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz",
+      "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz",
+      "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz",
+      "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz",
+      "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz",
+      "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz",
+      "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz",
+      "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz",
+      "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz",
+      "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz",
+      "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@shikijs/core": {
+      "version": "1.0.0-beta.3",
+      "resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-1.0.0-beta.3.tgz",
+      "integrity": "sha512-SCwPom2Wn8XxNlEeqdzycU93SKgzYeVsedjqDsgZaz4XiiPpZUzlHt2NAEQTwTnPcHNZapZ6vbkwJ8P11ggL3Q==",
+      "dev": true
+    },
+    "node_modules/@shikijs/transformers": {
+      "version": "1.0.0-beta.3",
+      "resolved": "https://registry.npmmirror.com/@shikijs/transformers/-/transformers-1.0.0-beta.3.tgz",
+      "integrity": "sha512-ASQQQqxW4dANxMGw4yGkTjtMSsUaRhImv/lzJEdfJ3/eP8TVlVYnohOFQVgpLjBBYGy9P0l0oKrlbjiGosTJ/Q==",
+      "dev": true,
+      "dependencies": {
+        "shiki": "1.0.0-beta.3"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.5.tgz",
+      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+      "dev": true
+    },
+    "node_modules/@types/linkify-it": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-3.0.5.tgz",
+      "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==",
+      "dev": true
+    },
+    "node_modules/@types/markdown-it": {
+      "version": "13.0.7",
+      "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-13.0.7.tgz",
+      "integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==",
+      "dev": true,
+      "dependencies": {
+        "@types/linkify-it": "*",
+        "@types/mdurl": "*"
+      }
+    },
+    "node_modules/@types/mdurl": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-1.0.5.tgz",
+      "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
+      "dev": true
+    },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.20",
+      "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+      "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+      "dev": true
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz",
+      "integrity": "sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==",
+      "dev": true,
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.4.15",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
+      "integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/parser": "^7.23.6",
+        "@vue/shared": "3.4.15",
+        "entities": "^4.5.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.0.2"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.4.15",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
+      "integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
+      "dev": true,
+      "dependencies": {
+        "@vue/compiler-core": "3.4.15",
+        "@vue/shared": "3.4.15"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.4.15",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
+      "integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/parser": "^7.23.6",
+        "@vue/compiler-core": "3.4.15",
+        "@vue/compiler-dom": "3.4.15",
+        "@vue/compiler-ssr": "3.4.15",
+        "@vue/shared": "3.4.15",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.5",
+        "postcss": "^8.4.33",
+        "source-map-js": "^1.0.2"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.4.15",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
+      "integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
+      "dev": true,
+      "dependencies": {
+        "@vue/compiler-dom": "3.4.15",
+        "@vue/shared": "3.4.15"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "7.0.14",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.0.14.tgz",
+      "integrity": "sha512-TluWR9qZ6aO11bwtYK8+fzXxBqLfsE0mWZz1q/EQBmO9k82Cm6deieLwNNXjNFJz7xutazoia5Qa+zTYkPPOfw==",
+      "dev": true,
+      "dependencies": {
+        "@vue/devtools-kit": "^7.0.14"
+      }
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "7.0.14",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.0.14.tgz",
+      "integrity": "sha512-wAAJazr4hI0aVRpgWOCVPw+NzMQdthhnprHHIg4njp1MkKrpCNGQ7MtQbZF1AltAA7xpMCGyyt+0kYH0FqTiPg==",
+      "dev": true,
+      "dependencies": {
+        "@vue/devtools-schema": "^7.0.14",
+        "@vue/devtools-shared": "^7.0.14",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1"
+      }
+    },
+    "node_modules/@vue/devtools-schema": {
+      "version": "7.0.14",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-schema/-/devtools-schema-7.0.14.tgz",
+      "integrity": "sha512-tpUeCLVrdHX+KzWMLTAwx/vAPFbo6jAUi7sr6Q+0mBIqIVSSIxNr5wEhegiFvYva+OtDeM2OrT+f7/X/5bvZNg==",
+      "dev": true
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "7.0.14",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.0.14.tgz",
+      "integrity": "sha512-79RP1NDakBVWou9rDpVnT1WMjTbL1lJKm6YEOodjQ0dq5ehf0wsRbeYDhgAlnjehWRzTq5GAYFBFUPYBs0/QpA==",
+      "dev": true,
+      "dependencies": {
+        "rfdc": "^1.3.1"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.4.15",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.4.15.tgz",
+      "integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
+      "dev": true,
+      "dependencies": {
+        "@vue/shared": "3.4.15"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.4.15",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
+      "integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
+      "dev": true,
+      "dependencies": {
+        "@vue/reactivity": "3.4.15",
+        "@vue/shared": "3.4.15"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.4.15",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
+      "integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
+      "dev": true,
+      "dependencies": {
+        "@vue/runtime-core": "3.4.15",
+        "@vue/shared": "3.4.15",
+        "csstype": "^3.1.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.4.15",
+      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
+      "integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
+      "dev": true,
+      "dependencies": {
+        "@vue/compiler-ssr": "3.4.15",
+        "@vue/shared": "3.4.15"
+      },
+      "peerDependencies": {
+        "vue": "3.4.15"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.4.15",
+      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.4.15.tgz",
+      "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
+      "dev": true
+    },
+    "node_modules/@vueuse/core": {
+      "version": "10.7.2",
+      "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.7.2.tgz",
+      "integrity": "sha512-AOyAL2rK0By62Hm+iqQn6Rbu8bfmbgaIMXcE3TSr7BdQ42wnSFlwIdPjInO62onYsEMK/yDMU8C6oGfDAtZ2qQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.20",
+        "@vueuse/metadata": "10.7.2",
+        "@vueuse/shared": "10.7.2",
+        "vue-demi": ">=0.14.6"
+      }
+    },
+    "node_modules/@vueuse/core/node_modules/vue-demi": {
+      "version": "0.14.7",
+      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.7.tgz",
+      "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vueuse/integrations": {
+      "version": "10.7.2",
+      "resolved": "https://registry.npmmirror.com/@vueuse/integrations/-/integrations-10.7.2.tgz",
+      "integrity": "sha512-+u3RLPFedjASs5EKPc69Ge49WNgqeMfSxFn+qrQTzblPXZg6+EFzhjarS5edj2qAf6xQ93f95TUxRwKStXj/sQ==",
+      "dev": true,
+      "dependencies": {
+        "@vueuse/core": "10.7.2",
+        "@vueuse/shared": "10.7.2",
+        "vue-demi": ">=0.14.6"
+      },
+      "peerDependencies": {
+        "async-validator": "*",
+        "axios": "*",
+        "change-case": "*",
+        "drauu": "*",
+        "focus-trap": "*",
+        "fuse.js": "*",
+        "idb-keyval": "*",
+        "jwt-decode": "*",
+        "nprogress": "*",
+        "qrcode": "*",
+        "sortablejs": "*",
+        "universal-cookie": "*"
+      },
+      "peerDependenciesMeta": {
+        "async-validator": {
+          "optional": true
+        },
+        "axios": {
+          "optional": true
+        },
+        "change-case": {
+          "optional": true
+        },
+        "drauu": {
+          "optional": true
+        },
+        "focus-trap": {
+          "optional": true
+        },
+        "fuse.js": {
+          "optional": true
+        },
+        "idb-keyval": {
+          "optional": true
+        },
+        "jwt-decode": {
+          "optional": true
+        },
+        "nprogress": {
+          "optional": true
+        },
+        "qrcode": {
+          "optional": true
+        },
+        "sortablejs": {
+          "optional": true
+        },
+        "universal-cookie": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vueuse/integrations/node_modules/vue-demi": {
+      "version": "0.14.7",
+      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.7.tgz",
+      "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "10.7.2",
+      "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.7.2.tgz",
+      "integrity": "sha512-kCWPb4J2KGrwLtn1eJwaJD742u1k5h6v/St5wFe8Quih90+k2a0JP8BS4Zp34XUuJqS2AxFYMb1wjUL8HfhWsQ==",
+      "dev": true
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "10.7.2",
+      "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.7.2.tgz",
+      "integrity": "sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==",
+      "dev": true,
+      "dependencies": {
+        "vue-demi": ">=0.14.6"
+      }
+    },
+    "node_modules/@vueuse/shared/node_modules/vue-demi": {
+      "version": "0.14.7",
+      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.7.tgz",
+      "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/algoliasearch": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/algoliasearch/-/algoliasearch-4.22.1.tgz",
+      "integrity": "sha512-jwydKFQJKIx9kIZ8Jm44SdpigFwRGPESaxZBaHSV0XWN2yBJAOT4mT7ppvlrpA4UGzz92pqFnVKr/kaZXrcreg==",
+      "dev": true,
+      "dependencies": {
+        "@algolia/cache-browser-local-storage": "4.22.1",
+        "@algolia/cache-common": "4.22.1",
+        "@algolia/cache-in-memory": "4.22.1",
+        "@algolia/client-account": "4.22.1",
+        "@algolia/client-analytics": "4.22.1",
+        "@algolia/client-common": "4.22.1",
+        "@algolia/client-personalization": "4.22.1",
+        "@algolia/client-search": "4.22.1",
+        "@algolia/logger-common": "4.22.1",
+        "@algolia/logger-console": "4.22.1",
+        "@algolia/requester-browser-xhr": "4.22.1",
+        "@algolia/requester-common": "4.22.1",
+        "@algolia/requester-node-http": "4.22.1",
+        "@algolia/transporter": "4.22.1"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+      "dev": true
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.12"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.19.12.tgz",
+      "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.19.12",
+        "@esbuild/android-arm": "0.19.12",
+        "@esbuild/android-arm64": "0.19.12",
+        "@esbuild/android-x64": "0.19.12",
+        "@esbuild/darwin-arm64": "0.19.12",
+        "@esbuild/darwin-x64": "0.19.12",
+        "@esbuild/freebsd-arm64": "0.19.12",
+        "@esbuild/freebsd-x64": "0.19.12",
+        "@esbuild/linux-arm": "0.19.12",
+        "@esbuild/linux-arm64": "0.19.12",
+        "@esbuild/linux-ia32": "0.19.12",
+        "@esbuild/linux-loong64": "0.19.12",
+        "@esbuild/linux-mips64el": "0.19.12",
+        "@esbuild/linux-ppc64": "0.19.12",
+        "@esbuild/linux-riscv64": "0.19.12",
+        "@esbuild/linux-s390x": "0.19.12",
+        "@esbuild/linux-x64": "0.19.12",
+        "@esbuild/netbsd-x64": "0.19.12",
+        "@esbuild/openbsd-x64": "0.19.12",
+        "@esbuild/sunos-x64": "0.19.12",
+        "@esbuild/win32-arm64": "0.19.12",
+        "@esbuild/win32-ia32": "0.19.12",
+        "@esbuild/win32-x64": "0.19.12"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "dev": true
+    },
+    "node_modules/focus-trap": {
+      "version": "7.5.4",
+      "resolved": "https://registry.npmmirror.com/focus-trap/-/focus-trap-7.5.4.tgz",
+      "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==",
+      "dev": true,
+      "dependencies": {
+        "tabbable": "^6.2.0"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+      "dev": true
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.6",
+      "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.6.tgz",
+      "integrity": "sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.4.15"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/mark.js": {
+      "version": "8.11.1",
+      "resolved": "https://registry.npmmirror.com/mark.js/-/mark.js-8.11.1.tgz",
+      "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==",
+      "dev": true
+    },
+    "node_modules/minisearch": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmmirror.com/minisearch/-/minisearch-6.3.0.tgz",
+      "integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==",
+      "dev": true
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "dev": true
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.7",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz",
+      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "dev": true,
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+      "dev": true
+    },
+    "node_modules/picocolors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz",
+      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+      "dev": true
+    },
+    "node_modules/postcss": {
+      "version": "8.4.33",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.33.tgz",
+      "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
+      "dev": true,
+      "dependencies": {
+        "nanoid": "^3.3.7",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/preact": {
+      "version": "10.19.3",
+      "resolved": "https://registry.npmmirror.com/preact/-/preact-10.19.3.tgz",
+      "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==",
+      "dev": true
+    },
+    "node_modules/rfdc": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.3.1.tgz",
+      "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==",
+      "dev": true
+    },
+    "node_modules/rollup": {
+      "version": "4.9.6",
+      "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.9.6.tgz",
+      "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==",
+      "dev": true,
+      "dependencies": {
+        "@types/estree": "1.0.5"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.9.6",
+        "@rollup/rollup-android-arm64": "4.9.6",
+        "@rollup/rollup-darwin-arm64": "4.9.6",
+        "@rollup/rollup-darwin-x64": "4.9.6",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.9.6",
+        "@rollup/rollup-linux-arm64-gnu": "4.9.6",
+        "@rollup/rollup-linux-arm64-musl": "4.9.6",
+        "@rollup/rollup-linux-riscv64-gnu": "4.9.6",
+        "@rollup/rollup-linux-x64-gnu": "4.9.6",
+        "@rollup/rollup-linux-x64-musl": "4.9.6",
+        "@rollup/rollup-win32-arm64-msvc": "4.9.6",
+        "@rollup/rollup-win32-ia32-msvc": "4.9.6",
+        "@rollup/rollup-win32-x64-msvc": "4.9.6",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/search-insights": {
+      "version": "2.13.0",
+      "resolved": "https://registry.npmmirror.com/search-insights/-/search-insights-2.13.0.tgz",
+      "integrity": "sha512-Orrsjf9trHHxFRuo9/rzm0KIWmgzE8RMlZMzuhZOJ01Rnz3D0YBAe+V6473t6/H6c7irs6Lt48brULAiRWb3Vw==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/shiki": {
+      "version": "1.0.0-beta.3",
+      "resolved": "https://registry.npmmirror.com/shiki/-/shiki-1.0.0-beta.3.tgz",
+      "integrity": "sha512-z7cHTNSSvwGx2DfeLwjSNLo+HcVxifgNIzLm6Ye52eXcIwNHXT0wHbhy7FDOKSKveuEHBwt9opfj3Hoc8LE1Yg==",
+      "dev": true,
+      "dependencies": {
+        "@shikijs/core": "1.0.0-beta.3"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz",
+      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/tabbable": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmmirror.com/tabbable/-/tabbable-6.2.0.tgz",
+      "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
+      "dev": true
+    },
+    "node_modules/vite": {
+      "version": "5.0.12",
+      "resolved": "https://registry.npmmirror.com/vite/-/vite-5.0.12.tgz",
+      "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
+      "dev": true,
+      "dependencies": {
+        "esbuild": "^0.19.3",
+        "postcss": "^8.4.32",
+        "rollup": "^4.2.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vitepress": {
+      "version": "1.0.0-rc.41",
+      "resolved": "https://registry.npmmirror.com/vitepress/-/vitepress-1.0.0-rc.41.tgz",
+      "integrity": "sha512-PAEoIIc9J//k/Wg39C6k86hZpXPmLZjRiTBwieDNeYGdevD7xr5Ve8o1W/w+e9dtyQMkuvzgianEamXDX3aj7g==",
+      "dev": true,
+      "dependencies": {
+        "@docsearch/css": "^3.5.2",
+        "@docsearch/js": "^3.5.2",
+        "@shikijs/core": "^1.0.0-beta.3",
+        "@shikijs/transformers": "^1.0.0-beta.3",
+        "@types/markdown-it": "^13.0.7",
+        "@vitejs/plugin-vue": "^5.0.3",
+        "@vue/devtools-api": "^7.0.14",
+        "@vueuse/core": "^10.7.2",
+        "@vueuse/integrations": "^10.7.2",
+        "focus-trap": "^7.5.4",
+        "mark.js": "8.11.1",
+        "minisearch": "^6.3.0",
+        "shiki": "^1.0.0-beta.3",
+        "vite": "^5.0.12",
+        "vue": "^3.4.15"
+      },
+      "bin": {
+        "vitepress": "bin/vitepress.js"
+      },
+      "peerDependencies": {
+        "markdown-it-mathjax3": "^4.3.2",
+        "postcss": "^8.4.33"
+      },
+      "peerDependenciesMeta": {
+        "markdown-it-mathjax3": {
+          "optional": true
+        },
+        "postcss": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.4.15",
+      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.4.15.tgz",
+      "integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==",
+      "dev": true,
+      "dependencies": {
+        "@vue/compiler-dom": "3.4.15",
+        "@vue/compiler-sfc": "3.4.15",
+        "@vue/runtime-dom": "3.4.15",
+        "@vue/server-renderer": "3.4.15",
+        "@vue/shared": "3.4.15"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    }
+  }
+}

+ 13 - 0
docs/package.json

@@ -0,0 +1,13 @@
+{
+  "name": "docs",
+  "description": "docs for springboot-note",
+  "devDependencies": {
+    "vitepress": "^1.0.0-rc.41"
+  },
+  "scripts": {
+    "dev": "vitepress dev --host",
+    "docs:dev": "vitepress dev",
+    "docs:build": "vitepress build",
+    "docs:preview": "vitepress preview"
+  }
+}

+ 25 - 0
scripts/rename_dir.py

@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@Contact :   liuyuqi.gov@msn.cn
+@Time    :   2024/02/02 16:39:55
+@License :   Copyright © 2017-2022 liuyuqi. All Rights Reserved.
+@Desc    :   rename dir spring-boot to springboot
+'''
+import os,sys,re
+
+workdir= os.getcwd()
+
+def run():
+    for root, dirs, files in os.walk(workdir):
+        for dir in dirs:
+            if dir.startswith('spring-boot'):
+                #rename dir
+                newdir = dir.replace('spring-boot','springboot')
+                os.rename(os.path.join(root,dir), os.path.join(root, newdir)) 
+            else:
+                print("sss")
+    pass
+
+if __name__=='__main__':
+    run()

Some files were not shown because too many files changed in this diff