保姆教程 Docker 部署微服务项目

打印 上一主题 下一主题

主题 528|帖子 528|积分 1584

大家好,我是奇兵。
文章比较长,请耐心看完!
项目上线是每位学编程同砚必须把握的根本技能。之前我已经给大家分享过许多种上线单体项目的方法了,今天再出一期微服务项目的部署教程,用一种最简单的方法,带大家轻松部署微服务项目。
开始之前,先做个小调研啊,大家更喜欢看 “真实踩坑版” 的教程照旧 “压缩纯净版” 的教程呢?
本期教程我照旧保持本身一直的风格,依然是保姆级教程,包罗了一些踩坑过程和解决方案,大家跟着做就完事儿~
传统部署

对于如许一个项目,假如我们还用传统单机项目的部署方式,一个个打 jar 包、用 Java 命令来启动,会有哪些问题呢?

  • 要一个个安装依赖,比如 MySQL 数据库、Redis、消息队列、Nacos,非常麻烦!
  • 要一个个打 jar 包、一个个手动运行 jar 包,非常麻烦!
  • 不方便会合观察所有服务的运行状态和资源占用情况
所以,为相识决这些问题,我们会选用一种更高效的微服务部署方式 —— Docker Compose。
Docker Compose 先容

在先容 Docker Compose 前,先简单先容下 Docker。
Docker 是一种容器技能,允许开发者将应用程序和所有依赖项(如代码、库、配置等)制作为 镜像。可以把镜像简单明白为软件安装包,可以在不同的计算机上通过它快速安装和启动应用程序(容器),这些程序独立隔离地运行,不受外部情况的影响。


假如要部署微服务项目,可能要启动多个 Docker 容器,比如 MySQL 容器、用户服务容器等。这时就需要 Docker Compose 了。它是一个容器编排助手,用于会合管理多个 Docker 容器的启动和协同工作。可以在一个配置文件中会合定义所有容器以及它们的关系。然后,可以使用一行命令启动所有容器,而不需要手动运行多个命令。


需要注意的是,Docker Compose 通常实用于把所有微服务部署在同一台服务器的场景,在真实的企业级项目中,每每会使用 K8S 等更专业的容器编排和自动化部署工具,更方便地在多个服务器上部署容器。
部署流程

相识了 Docker 和 Docker Compose 的作用后,我们来快速相识下部署流程,分为 2 大阶段 —— 本地部署和服务器部署。
一、本地部署

  • 梳理服务部署表格
  • Maven 子父模块打包
  • Dockerfile 编写
  • 编写情况依赖配置
  • 编写服务配置
  • 调解程序配置
  • 测试访问
二、服务端部署

  • 准备服务器
  • Docker Compose 安装
  • 同步文件
  • 获取 jar 包
  • 服务启动
  • 测试访问
一、本地部署

第一阶段是本地部署,也可以叫做部署准备。
猛烈建议大家,比起直接操作线上服务器,最好是先在本地把所有的流程跑通,风险更低、服从更高。
这里我使用的是 Mac 操作系统,已经安装了 Docker Desktop 软件,管理 Docker 容器会更方便一些。


对于本地没有 Docker 情况的同砚,这一阶段细致看一遍有个印象就足够了。可以直接拿我调试好的配置文件在服务器上部署,而不消本身调试。



1.1、梳理服务部署表格

在部署微服务项目前,首先要规划好要部署哪些服务、以及各服务的关键信息,比如服务名称、版本号、占用端口号、关键配置等。
对于我的在线判题项目,梳理好的服务表格如下:
服务名称英文名端口号版本号服务类别数据库mysql3306v8情况依赖缓存redis6379v6情况依赖消息队列rabbitmq5672, 15672v3.12.6情况依赖注册中心nacos8848v2.2.0情况依赖网关服务gateway8101java 8业务服务用户服务yuoj-backend-user-service8102java 8业务服务标题服务yuoj-backend-question-service8103java 8业务服务判题服务yuoj-backend-judge-service8104java 8业务服务 为什么这里我要划分服务类别为 “情况依赖” 和 “业务服务” 呢?
因为在启动服务时,必须要先启动情况依赖,才气启动业务服务,否则就会报类似 “无法连接数据库” 之类的错误。
1.2、Maven 子父模块打包

对于微服务项目,我们通常是使用 Maven 的子父模块功能举行管理的。需要部署项目时,不消针对每个子服务单独执行 mvn package 命令举行打包,而是可以一键打包所有服务。
想要实现这个功能,需要给子父模块的依赖文件(pom.xml)举行一些配置,重要包罗:
1)父模块配置

在父模块的 pom.xml 文件中引入 spring-boot-maven-plugin 即可,注意一定不要配置 configuration 和 repackage!
示例代码如下:
  1. <plugin>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-maven-plugin</artifactId>
  4.     <version>${spring-boot.version}</version>
  5. </plugin>
复制代码
2)子模块配置

修改所有需要启动 Spring Boot 的服务(用户服务、标题服务、判题服务、网关服务)的子模块 pom.xml 文件。
重要是增加 executions 配置,使用 spring-boot-maven-plugin 的 repackage 命令来构建子模块,从而自动在构建时将公共模块的依赖打入 jar 包。
示例代码如下:
  1. <plugin>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-maven-plugin</artifactId>
  4.     <executions>
  5.         <execution>
  6.             <id>repackage</id>
  7.             <goals>
  8.                 <goal>repackage</goal>
  9.             </goals>
  10.         </execution>
  11.     </executions>
  12. </plugin>
复制代码
1.3、Dockerfile 编写

Dockerfile 是定义 Docker 容器镜像构建过程的文件,包罗容器镜像使用的基础情况、容器内的依赖和文件、容器的配置、启动命令等。
有了 Dockerfile,我们就能很轻松地制作出本身的容器镜像。
固然 Dockerfile 的写法并不复杂,但我照旧建议大家只管不要本身写,直接去网上找个差不多的项目,复制粘贴别人的 Dockerfile 就足够了!
给大家提供 2 种常用的 Spring Boot 项目的 Dockerfile。
1)复制 jar 包版

思路:在本地打好 jar 包后,复制 jar 包到容器中运行
示例代码如下:
  1. # 基础镜像
  2. FROM openjdk:8-jdk-alpine
  3. # 指定工作目录
  4. WORKDIR /app
  5. # 将 jar 包添加到工作目录,比如 target/yuoj-backend-user-service-0.0.1-SNAPSHOT.jar
  6. ADD {本地 jar 包路径} . 
  7. # 暴露端口
  8. EXPOSE {服务端口号}
  9. # 启动命令
  10. ENTRYPOINT ["java","-jar","/app/{jar 包名称}","--spring.profiles.active=prod"]
复制代码
2)Maven 打包版

思路:复制本地代码到容器中,在容器中使用 Maven 打包再运行
示例代码如下:
  1. # 基础镜像
  2. FROM maven:3.8.1-jdk-8-slim as builder
  3. # 指定工作目录
  4. WORKDIR /app
  5. # 添加源码文件
  6. COPY pom.xml .
  7. COPY src ./src
  8. # 构建 jar 包,跳过测试
  9. RUN mvn package -DskipTests
  10. # 启动命令
  11. ENTRYPOINT ["java","-jar","/app/target/{jar 包名称}","--spring.profiles.active=prod"]
复制代码
此处由于我们的微服务项目可以一键打好所有子服务的 jar 包,就没须要每个服务单独在容器中打包了,所以选择第一种方式的 Dockerfile。
我们需要给每个 Spring Boot 服务(用户服务、标题服务、判题服务、网关服务)都编写一个 Dockerfile,放到每个子服务的根目录下。


以用户服务为例,示例代码如下:
  1. # 基础镜像
  2. FROM openjdk:8-jdk-alpine
  3.   
  4. # 指定工作目录
  5. WORKDIR /app
  6.   
  7. # 将 jar 包添加到工作目录,比如 target/yuoj-backend-user-service-0.0.1-SNAPSHOT.jar
  8. ADD target/yuoj-backend-user-service-0.0.1-SNAPSHOT.jar .
  9.   
  10. # 暴露端口
  11. EXPOSE 8102
  12.   
  13. # 启动命令
  14. ENTRYPOINT ["java","-jar","/app/yuoj-backend-user-service-0.0.1-SNAPSHOT.jar","--spring.profiles.active=prod"]
复制代码
建议先在本地利用 IDEA 开发工具调通镜像构建流程,确保每个 Dockerfile 都是可以乐成制作镜像的:


检察容器的启动日志,发现可以或许启动服务、看到 Spring 图标即可:


1.4、编写情况依赖配置

接下来,我们就要编写 Docker Compose 的配置文件了,可以根据配置文件快速启动多个服务。
之前我们已经梳理了服务部署表格,将服务划分为了 “情况依赖” 和 “业务服务”。
由于业务服务依赖 MySQL 等情况依赖,所以需要拆分 2 套 Docker Compose 的配置文件,分别为 docker-compose.env.yml 情况配置和 docker-compose.service.yml 业务服务配置,包管先乐成启动依赖,再启动服务。
学过 Docker Compose 的同砚可能听说过 depends_on 配置,也能决定服务的启动顺序。但是万万注意,depends_on 并不会等待服务完全停当,只是确保它们在启动时的顺序,并不稳定。
怎样编写 Docker Compose 文件呢?
和 Dockerfile 一样,直接去网上找现成的 Docker Compose file,复制粘贴过来略做修改就能使用了~
再配合以下 2 个网站,完全无需记忆 Docker Compose 的写法!


  • Docker Compose file 官方文档:https://docs.docker.com/compose/compose-file/
  • 搜刮现成的 Docker 镜像:https://hub.docker.com/
当然,如今 AI 时代了,尚有更简单的方式!
直接把我们整理好的服务部署需要喂给 GPT,让 AI 帮我们生成配置即可~
示例 prompt:
  1. 现在我需要用 docker compose 来部署 mysql 8(3306 端口)username=root,password=123456
  2. redis 6(无密码,6379端口)、rabbitmq v.3.12.6( 5672 端口   password: guest,username: guest)、nacos 2.2.0(8848端口);还有 4 个本地的 springboot 服务(名称分别为:yuoj-backend-user-service 8102端口、yuoj-backend-question-service 8103端口、yuoj-backend-judge-service 8104端口、yuoj-backend-gateway 8101 端口),每个服务本地目录都有一个 Dockerfile,请帮我自动生成 docker compose 的 yml 配置文件,要求这些服务网络能够连通
复制代码
效果照旧非常不错的,只要你形貌地足够清楚,生成的配置完全可用!


由于这篇文章是教程嘛,我就带大家通过调试的方式一步步完成 Docker Compose 文件,最后会把完备的 Docker Compose 文件给大家分享出来,大家直接用就行了~
我们要分别在 Docker Compose 中定义 4 大基础依赖,包罗 MySQL、Redis、RabbitMQ 和 Nacos。
1)MySQL

我们不仅要创建一个 MySQL 服务,还要在创建服务后自动创建我们需要的库表布局。
所以需要先准备数据库 SQL 脚本文件,里面包罗了建库、建表语句,我们把它放在微服务项目根目录的 mysql-init 文件夹中:



由于要在本地启动 MySQL,还需要定义一个文件夹 .mysql-data 来存放 MySQL 的恒久化数据,防止容器重启后数据丢失。
做好这两点后,就可以编写 docker-compose.env.yml 文件了,先只写一个 MySQL 服务,示例代码如下:
关键配置的含义我都写到注释里了
  1. version: '3'
  2. services:
  3.   mysql:
  4.     image: mysql:8 # 使用的镜像
  5.     container_name: yuoj-mysql # 启动的实例名称
  6.     environment:
  7.       MYSQL_ROOT_PASSWORD: 123456 # root 用户密码
  8.     ports:
  9.       - "3306:3306" # 端口映射
  10.     volumes:
  11.       - ./.mysql-data:/var/lib/mysql # 将数据目录挂载到本地目录以进行持久化
  12.       - ./mysql-init:/docker-entrypoint-initdb.d # 自动执行启动脚本
  13.     restart: always # 崩溃后自动重启
  14.     networks:
  15.       - mynetwork # 指定网络
  16. networks:
  17.   mynetwork: # 自定义网络,实现网络互通和隔离
复制代码
写好配置文件后,可以直接在 IDEA 里执行 Docker Compose 文件,调试 MySQL 的运行:


运行乐成后,我们可以在本地乐成连接数据库:


2)Redis

Redis 服务的定义和启动操作和 MySQL 服务几乎一致,Redis 的 Docker Compose 配置示例代码如下:
  1. version: '3'
  2. services:
  3.   redis:
  4.     image: redis:6
  5.     container_name: yuoj-redis
  6.     ports:
  7.       - "6379:6379"
  8.     networks:
  9.       - mynetwork
  10.     volumes:
  11.       - ./.redis-data:/data # 持久化
  12. networks:
  13.   mynetwork:
复制代码
然后在本地执行 Docker Compose 文件,启动 Redis 服务,并且尝试进入 Terminal 来调试 Redis:


3)RabbitMQ


同 MySQL 和 Redis,RabbitMQ 的 Docker Compose 配置示例代码如下:
  1. version: '3'
  2. services:
  3.   rabbitmq:
  4.     image: rabbitmq:3.12.6-management # 支持管理面板的消息队列
  5.     container_name: yuoj-rabbitmq
  6.     environment:
  7.       RABBITMQ_DEFAULT_USER: guest
  8.       RABBITMQ_DEFAULT_PASS: guest
  9.     ports:
  10.       - "5672:5672"
  11.       - "15672:15672" # RabbitMQ Dashboard 端口
  12.     volumes:
  13.       - ./.rabbitmq-data:/var/lib/rabbitmq # 持久化
  14.     networks:
  15.       - mynetwork
  16. networks:
  17.   mynetwork:
复制代码
本地执行 Docker Compose 文件,启动 RabbitMQ 服务,然后可以访问 localhost:15672 检察到管理面板,就表现启动乐成了~
账号密码都是 guest


4)Nacos

和其他服务一样,Nacos 也需要编写 Docker Compose 配置。
但是在选择 Nacos 镜像时必须要注意,建议选择支持 linux/arm64 架构的镜像版本,比如 v2.2.0-slim,否则后面可能会无法运行:


Nacos 示例配置文件如下:
  1. version: '3'
  2. services:
  3.   nacos:
  4.     image: nacos/nacos-server:v2.2.0-slim
  5.     container_name: yuoj-nacos
  6.     ports:
  7.       - "8848:8848"
  8.     volumes:
  9.       - ./.nacos-data:/home/nacos/data
  10.     networks:
  11.       - mynetwork
  12.     environment:
  13.       - MODE=standalone # 单节点模式启动
  14.       - PREFER_HOST_MODE=hostname # 支持 hostname
  15.       - TZ=Asia/Shanghai # 控制时区
  16. networks:
  17.   mynetwork:
复制代码
然后在本地执行 Docker Compose 启动 Nacos,访问 localhost:8848/nacos 可以或许看到管理页面,就表现运行乐成了~
管理页面的账号和密码默认都是 nacos


完备 Docker Compose 文件

分别调试完上述服务后,我们把所有的配置拼在一起,就得到了完备的文件,文件名为 docker-compose.env.yml。
完备代码如下:
  1. version: '3'
  2. services:
  3.   mysql:
  4.     image: mysql:8 # 使用的镜像
  5.     container_name: yuoj-mysql # 启动的实例名称
  6.     environment:
  7.       MYSQL_ROOT_PASSWORD: 123456 # root 用户密码
  8.     ports:
  9.       - "3306:3306" # 端口映射
  10.     volumes:
  11.       - ./.mysql-data:/var/lib/mysql # 将数据目录挂载到本地目录以进行持久化
  12.       - ./mysql-init:/docker-entrypoint-initdb.d # 启动脚本
  13.     restart: always # 崩溃后自动重启
  14.     networks:
  15.       - mynetwork # 指定网络
  16.   redis:
  17.     image: redis:6
  18.     container_name: yuoj-redis
  19.     ports:
  20.       - "6379:6379"
  21.     networks:
  22.       - mynetwork
  23.     volumes:
  24.       - ./.redis-data:/data # 持久化
  25.   rabbitmq:
  26.     image: rabbitmq:3.12.6-management # 支持管理面板的消息队列
  27.     container_name: yuoj-rabbitmq
  28.     environment:
  29.       RABBITMQ_DEFAULT_USER: guest
  30.       RABBITMQ_DEFAULT_PASS: guest
  31.     ports:
  32.       - "5672:5672"
  33.       - "15672:15672" # RabbitMQ Dashboard 端口
  34.     volumes:
  35.       - ./.rabbitmq-data:/var/lib/rabbitmq # 持久化
  36.     networks:
  37.       - mynetwork
  38.   nacos:
  39.     image: nacos/nacos-server:v2.2.0-slim
  40.     container_name: yuoj-nacos
  41.     ports:
  42.       - "8848:8848"
  43.     volumes:
  44.       - ./.nacos-data:/home/nacos/data
  45.     networks:
  46.       - mynetwork
  47.     environment:
  48.       - MODE=standalone # 单节点模式启动
  49.       - PREFER_HOST_MODE=hostname # 支持 hostname
  50.       - TZ=Asia/Shanghai # 控制时区
  51. networks:
  52.   mynetwork:
复制代码
1.5、编写业务服务配置

用同样的方式,我们可以编写业务服务的 Docker Compose 文件,文件名称 docker-compose.service.yml。
示例代码如下,其中需要格外关注的配置是 build 和 depends_on:
  1. version: '3'
  2. services:
  3.   yuoj-backend-gateway:
  4.     container_name: yuoj-backend-gateway
  5.     build: # 服务的 Docker 构建文件位置
  6.       context: ./yuoj-backend-gateway
  7.       dockerfile: Dockerfile
  8.     ports:
  9.       - "8101:8101"
  10.     networks:
  11.       - mynetwork
  12.   
  13.   yuoj-backend-user-service:
  14.     container_name: yuoj-backend-user-service
  15.     build:
  16.       context: ./yuoj-backend-user-service
  17.       dockerfile: Dockerfile
  18.     ports:
  19.       - "8102:8102"
  20.     networks:
  21.       - mynetwork
  22.     depends_on: # 本服务依赖的服务,控制启动先后顺序
  23.       - yuoj-backend-gateway
  24.   yuoj-backend-question-service:
  25.     container_name: yuoj-backend-question-service
  26.     build:
  27.       context: ./yuoj-backend-question-service
  28.       dockerfile: Dockerfile
  29.     ports:
  30.       - "8103:8103"
  31.     networks:
  32.       - mynetwork
  33.     depends_on:
  34.       - yuoj-backend-user-service
  35.       - yuoj-backend-gateway
  36.   yuoj-backend-judge-service:
  37.     container_name: yuoj-backend-judge-service
  38.     build:
  39.       context: ./yuoj-backend-judge-service
  40.       dockerfile: Dockerfile
  41.     ports:
  42.       - "8104:8104"
  43.     networks:
  44.       - mynetwork
  45.     depends_on:
  46.       - yuoj-backend-user-service
  47.       - yuoj-backend-question-service
  48.       - yuoj-backend-gateway
  49. # 网络,不定义的话就是默认网络
  50. networks:
  51.   mynetwork:
复制代码
1.6、调解程序配置

编写好上述配置文件后,本地尝试运行 Docker Compose 业务服务,结果发现:报错啦!依赖服务的地点访问不通!
这是由于之前我们的项目访问依赖服务时,全部是使用了固定的 IP 地点(比如 localhost),而容器内部的 localhost(或 127.0.0.1)通常指向容器本身,而不是宿主主机。所以为了在容器内访问其他服务,程序中应该使用服务名称而不是 localhost。
我们给每个 Spring Boot 服务都增加一套 prod 上线配置,在配置中更改服务调用地点。
用户服务、标题服务和判题服务的 application-prod.yml 配置修改如下:
  1. # 生产环境配置文件
  2. spring:
  3.   # 数据库配置
  4.   datasource:
  5.     driver-class-name: com.mysql.cj.jdbc.Driver
  6.     url: jdbc:mysql://mysql:3306/yuoj # localhost 改为 mysql
  7.     username: root
  8.     password: 123456
  9.   # Redis 配置
  10.   redis:
  11.     database: 1
  12.     host: redis # localhost 改为 redis
  13.     port: 6379
  14.     timeout: 5000
  15.   cloud:
  16.     nacos:
  17.       discovery:
  18.         server-addr: nacos:8848 # localhost 改为 nacos
  19.   rabbitmq:
  20.     host: rabbitmq # localhost 改为 rabbitmq
  21.     port: 5672
  22.     password: guest
  23.     username: guest
复制代码
Gateway 网关服务的配置修改如下:
  1. spring:
  2.   cloud:
  3.     nacos:
  4.       discovery:
  5.         server-addr: nacos:8848 # localhost 改为 nacos
  6.     gateway:
  7.       routes:
  8.         - id: yuoj-backend-user-service
  9.           uri: lb://yuoj-backend-user-service
  10.           predicates:
  11.             - Path=/api/user/**
  12.         - id: yuoj-backend-question-service
  13.           uri: lb://yuoj-backend-question-service
  14.           predicates:
  15.             - Path=/api/question/**
  16.         - id: yuoj-backend-judge-service
  17.           uri: lb://yuoj-backend-judge-service
  18.           predicates:
  19.             - Path=/api/judge/**
  20.   application:
  21.     name: yuoj-backend-gateway
  22.   main:
  23.     web-application-type: reactive
  24. server:
  25.   port: 8101
  26. knife4j:
  27.   gateway:
  28.     enabled: true
  29.     strategy: discover
  30.     discover:
  31.       enabled: true
  32.       version: swagger2
复制代码
然后执行 mvn package 命令重新打包、执行 Docker Compose。
结果发现大多数服务都启动了,但是判题服务尚有报错。


这是因为程序在创建消息队列时存在硬编码的变量,指定了 host 为 "localhost",示例代码如下:
  1. ConnectionFactory factory = new ConnectionFactory();
  2. factory.setHost("localhost");
  3. Connection connection = factory.newConnection();
  4. Channel channel = connection.createChannel();
  5. String EXCHANGE_NAME = "code_exchange";
  6. channel.exchangeDeclare(EXCHANGE_NAME, "direct");
复制代码
举这个例子是为了告诉大家,在程序中只管不要写固定 IP 或域名,全部改为动态读取配置文件,便于修改。
示例修改后的代码如下:
  1. /**
  2.  * 用于创建测试程序用到的交换机和队列(只用在程序启动前执行一次)
  3.  */
  4. @Slf4j
  5. @Component
  6. public class InitRabbitMqBean {
  7.     @Value("${spring.rabbitmq.host:localhost}")
  8.     private String host;
  9.     @PostConstruct
  10.     public void init() {
  11.         try {
  12.             ConnectionFactory factory = new ConnectionFactory();
  13.             factory.setHost(host);
  14.             Connection connection = factory.newConnection();
  15.             Channel channel = connection.createChannel();
  16.             String EXCHANGE_NAME = "code_exchange";
  17.             channel.exchangeDeclare(EXCHANGE_NAME, "direct");
  18.             // 创建队列,随机分配一个队列名称
  19.             String queueName = "code_queue";
  20.             channel.queueDeclare(queueName, true, false, false, null);
  21.             channel.queueBind(queueName, EXCHANGE_NAME, "my_routingKey");
  22.             log.info("消息队列启动成功");
  23.         } catch (Exception e) {
  24.             log.error("消息队列启动失败");
  25.         }
  26.     }
  27. }
复制代码
1.7、测试访问

修复上述问题后,所有服务都可以通过 Docker Compose 文件启动了。
然后我们访问 localhost:8101/doc.html 网关地点,可以或许看到 Swagger 聚合接口文档。


依次调用用户注册 => 登录 => 获取登录用户信息 => 创建标题接口,全部执行乐成。
至此,第一阶段就完成啦。
二、服务器部署

在第二阶段,我们的目的就是在真实的 Linux 服务器上部署微服务项目。有了第一阶段的准备,第二阶段简直可以说是轻而易举!
2.1、准备服务器

首先,我们要有一台 Linux 服务器。
选择服务器前,我们必须要评估下微服务项目的内存占用,万万别把服务器的内存买小了!
可以使用 Docker Desktop 直接检察内存占用,虚拟机内存大概占用了 8 个 G、容器实际内存占用了 4 个 G:


那我们搞多少内存的服务器呢?
我猜许多同砚会说 8 G,奈何我天生反骨,明知山有虎偏向虎山行(重要是想省

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

河曲智叟

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表