array(2) { ["docs"]=> array(10) { [0]=> array(10) { ["id"]=> string(3) "428" ["text"]=> string(77) "Visual Studio 2017 单独启动MSDN帮助(Microsoft Help Viewer)的方法" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(8) "DonetRen" ["tagsname"]=> string(55) "Visual Studio 2017|MSDN帮助|C#程序|.NET|Help Viewer" ["tagsid"]=> string(23) "[401,402,403,"300",404]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400964" ["_id"]=> string(3) "428" } [1]=> array(10) { ["id"]=> string(3) "427" ["text"]=> string(42) "npm -v;报错 cannot find module "wrapp"" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(4) "zzty" ["tagsname"]=> string(50) "node.js|npm|cannot find module "wrapp“|node" ["tagsid"]=> string(19) "[398,"239",399,400]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400760" ["_id"]=> string(3) "427" } [2]=> array(10) { ["id"]=> string(3) "426" ["text"]=> string(54) "说说css中pt、px、em、rem都扮演了什么角色" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(12) "zhengqiaoyin" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400640" ["_id"]=> string(3) "426" } [3]=> array(10) { ["id"]=> string(3) "425" ["text"]=> string(83) "深入学习JS执行--创建执行上下文(变量对象,作用域链,this)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "Ry-yuan" ["tagsname"]=> string(33) "Javascript|Javascript执行过程" ["tagsid"]=> string(13) "["169","191"]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511399901" ["_id"]=> string(3) "425" } [4]=> array(10) { ["id"]=> string(3) "424" ["text"]=> string(30) "C# 排序技术研究与对比" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(9) "vveiliang" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(8) ".Net Dev" ["catesid"]=> string(5) "[199]" ["createtime"]=> string(10) "1511399150" ["_id"]=> string(3) "424" } [5]=> array(10) { ["id"]=> string(3) "423" ["text"]=> string(72) "【算法】小白的算法笔记:快速排序算法的编码和优化" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(9) "penghuwan" ["tagsname"]=> string(6) "算法" ["tagsid"]=> string(7) "["344"]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511398109" ["_id"]=> string(3) "423" } [6]=> array(10) { ["id"]=> string(3) "422" ["text"]=> string(64) "JavaScript数据可视化编程学习(二)Flotr2,雷达图" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "chengxs" ["tagsname"]=> string(28) "数据可视化|前端学习" ["tagsid"]=> string(9) "[396,397]" ["catesname"]=> string(18) "前端基本知识" ["catesid"]=> string(5) "[198]" ["createtime"]=> string(10) "1511397800" ["_id"]=> string(3) "422" } [7]=> array(10) { ["id"]=> string(3) "421" ["text"]=> string(36) "C#表达式目录树(Expression)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(4) "wwym" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(4) ".NET" ["catesid"]=> string(7) "["119"]" ["createtime"]=> string(10) "1511397474" ["_id"]=> string(3) "421" } [8]=> array(10) { ["id"]=> string(3) "420" ["text"]=> string(47) "数据结构 队列_队列实例:事件处理" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "idreamo" ["tagsname"]=> string(40) "C语言|数据结构|队列|事件处理" ["tagsid"]=> string(23) "["246","247","248",395]" ["catesname"]=> string(12) "数据结构" ["catesid"]=> string(7) "["133"]" ["createtime"]=> string(10) "1511397279" ["_id"]=> string(3) "420" } [9]=> array(10) { ["id"]=> string(3) "419" ["text"]=> string(47) "久等了,博客园官方Android客户端发布" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(3) "cmt" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511396549" ["_id"]=> string(3) "419" } } ["count"]=> int(200) } 222 社交网站后端项目开发日记(一) - 爱码网

本项目目标是开发一个社区网站,拥有发帖、讨论、搜索、登录等一个正常社区拥有的功能。涉及到的版本参数为:

  • JDK1.8
  • Maven3.8.1(直接集成到IDEA)
  • Springboot 2.5.1
  • tomcat

参考网站(在使用框架过程中可能会看的开发文档):

https://mvnrepository.com/ 查找maven依赖

https://mybatis.org/mybatis-3/zh/index.html mybatis的官方文档,配置等都有说明

项目代码已发布到github https://github.com/GaoYuan-1/web-project

关于数据库文件,该篇博客中已有提到,可去文中github获取数据 MySQL基础篇(一)

本文第一篇只介绍了基础,在(二)中将会介绍如何实现登录界面等后续内容。最终将会把完整项目经历发布出来。

本系列主要介绍的是实战内容,对于理论知识介绍较少,适合有一定基础的人。

首先创建一个项目,可利用Spring Initializr

本人配置如下:

社交网站后端项目开发日记(一)

maven项目的 pom.xml 中初始依赖如下,后面会增加更多依赖。

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

   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
   </dependency>
</dependencies>

记录一些基本概念,Spring的知识可能说的比较少,本篇关注于如何对网站进行实现。

Spring Bean是被实例的,组装的及被Spring 容器管理的Java对象。

Spring 容器会自动完成@bean对象的实例化。

创建应用对象之间的协作关系的行为称为:装配(wiring),这就是依赖注入的本质。

在使用 Spring Initializr 之后,第一个接触的注解为:

 
@SpringBootApplication

我们可以把 @SpringBootApplication 看作是 @Configuration@EnableAutoConfiguration@ComponentScan 注解的集合。

根据 SpringBoot 官网,这三个注解的作用分别是:

  • @EnableAutoConfiguration :启用 SpringBoot 的自动配置机制
  • @ComponentScan : 扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描该类所在的包下所有的类。
  • @Configuration :允许在 Spring 上下文中注册额外的 bean 或导入其他配置类(表示该类是配置类)

Spring通过识别一些注解可以让容器自动装填bean:

  • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。
  • @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao (Data Access Object)层。
  • @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。

关于@Scope,负责声明Bean的作用域:

 
@Scope("singleton")  //唯一 bean 实例,Spring 中的 bean 默认都是单例的。
@Scope("prototype")  //每次请求都会创建一个新的 bean 实例。
@Scope("request")    //每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
@Scope("session")    //每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。

关于@Autowired:

自动导入对象到类中,被注入进的类同样要被 Spring 容器管理。从构造器,到方法,到参数、属性、注解,都可以加上@Autowired注解。

2. SpringMVC相关

可以先了解一些HTTP流的相关知识:(参考自MDN Web Docs)

当客户端想要和服务端进行信息交互时(服务端是指最终服务器,或者是一个中间代理),过程表现为下面几步:

  1. 打开一个TCP连接:TCP连接被用来发送一条或多条请求,以及接受响应消息。客户端可能打开一条新的连接,或重用一个已经存在的连接,或者也可能开几个新的TCP连接连向服务端。

  2. 发送一个HTTP报文:HTTP报文(在HTTP/2之前)是语义可读的。在HTTP/2中,这些简单的消息被封装在了帧中,这使得报文不能被直接读取,但是原理仍是相同的。

     
    GET / HTTP/1.1
    Host: developer.mozilla.org
    Accept-Language: fr
    
  3. 读取服务端返回的报文信息:

     
    HTTP/1.1 200 OK
    Date: Sat, 09 Oct 2010 14:28:02 GMT
    Server: Apache
    Last-Modified: Tue, 01 Dec 2009 20:18:22 GMT
    ETag: "51142bc1-7449-479b075b2891b"
    Accept-Ranges: bytes
    Content-Length: 29769
    Content-Type: text/html
    
    <!DOCTYPE html... (here comes the 29769 bytes of the requested web page)
    
  4. 关闭连接或者为后续请求重用连接。

当HTTP流水线启动时,后续请求都可以不用等待第一个请求的成功响应就被发送。然而HTTP流水线已被证明很难在现有的网络中实现,因为现有网络中有很多老旧的软件与现代版本的软件共存。因此,HTTP流水线已被在有多请求下表现得更稳健的HTTP/2的帧所取代。

HTTP请求的例子:

社交网站后端项目开发日记(一)

响应的例子:

社交网站后端项目开发日记(一)

2.1 SpringMVC概要

三层架构:表现层,业务层和数据访问层。

MVC:Model(模型层),View(视图层),Controller(控制层)

社交网站后端项目开发日记(一)

核心组件: DispatcherServlet

2.2 Thymeleaf思想

如果想给浏览器返回一个动态网页,则需要一个工具支持,例如:Thymeleaf(模板引擎,生成动态的HTML)。

社交网站后端项目开发日记(一)

该引擎需要学习的有:标准表达式,判断与循环,模板的布局。

学习建议参考官方文档 https://www.thymeleaf.org/index.html

2.3 SpringMVC代码示例

GET请求

@RequestMapping支持Servlet的request和response作为参数,以下为一个简单示例:

 
@Controller
@RequestMapping("/alpha")
public class AlphaController {
    @RequestMapping("/hello")
    @ResponseBody
    public String sayHello(){
        return "Hello Spring Boot.";
    }

    @RequestMapping("/http")
    public void http(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //获取请求数据
        System.out.println(request.getMethod());
        System.out.println(request.getServletPath());//请求路径
        Enumeration<String> enumeration = request.getHeaderNames();//得到请求行的key
        while(enumeration.hasMoreElements()) {
            String name = enumeration.nextElement(); //当前值(key)
            String value = request.getHeader(name);//得到value
            System.out.println(name + ":" + value);
        }
        System.out.println(request.getParameter("code"));

        // 返回响应数据
        response.setContentType("text/html;charset=utf-8");//返回网页类型的文本
        PrintWriter writer = response.getWriter();
        writer.write("<h1>牛客网</h1>");//这里只进行简单输出
        writer.close();
    }
}

在项目Controller层加入代码,以response体返回一个html的文本。

社交网站后端项目开发日记(一)

这是通过底层对象处理请求的方式,便于理解。

更简单的方式为:

 
// GET请求,用于获取某些数据
    // /students?current=1&limit=20 假设查询学生数据,第一页,每页20条
    @RequestMapping(path = "/students", method = RequestMethod.GET)
    @ResponseBody
//    public String getStudents(int current,int limit) { //直接使用Int类型,前端控制器会自动识别匹配
//        System.out.println(current);
//        System.out.println(limit);
//        return "some students";
//    }
//    也可加上注解
    public String getStudents(
            @RequestParam(name = "current", required = false, defaultValue = "1") int current,
            @RequestParam(name = "limit", required = false, defaultValue = "1")  int limit) {
        System.out.println(current);
        System.out.println(limit);
        return "some students";
    }

利用@ResponseBody注解,实现效果为:

社交网站后端项目开发日记(一)

控制台返回结果:

社交网站后端项目开发日记(一)

利用这种方式,不论是返回数据还是获取数据都很方便。

另外,通过路径方式传参查询的话:(注意,路径长度并不是无限的,并不可以传很多参数)

 
// /student/123  查询某个学生,直接放在路径中
@RequestMapping(path = "/student/{id}", method = RequestMethod.GET)
@ResponseBody
public String getStudent(@PathVariable("id") int id) {
    System.out.println(id);
    return "a student";
}

注意以上传参时使用的两个注解。

POST请求

如何向浏览器提交数据?

在项目静态文件中添加html文件,action指示路径

 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>增加学生</title>
</head>
<body>

    <form method="post" action="/community/alpha/student">
        <p>
            姓名:<input type="text" name="name">
        </p>
        <p>
            年龄:<input type="text" name="age">
        </p>
        <p>
            <input type="submit" value="保存">
        </p>
    </form>

</body>
</html>
 
//POST请求
@RequestMapping(path = "/student", method = RequestMethod.POST)
@ResponseBody
public String saveStudent(String name, int age) {
    System.out.println(name);
    System.out.println(age);
    return "success";
}

在Controller中代码如下,展示效果:

社交网站后端项目开发日记(一)

控制台:

社交网站后端项目开发日记(一)

响应HTML数据

从程序内部响应到HTML

 
//响应HTML数据
@RequestMapping(path = "/teacher", method = RequestMethod.GET)
public ModelAndView getTeacher() {   //这个对象就是向前端控制器返回的moder和view
    ModelAndView mav = new ModelAndView();
    mav.addObject("name", "张三");
    mav.addObject("age", 30);
    mav.setViewName("/demo/view");//这个view实际上指的是view.html
    return mav;
}

Controller代码

此外,还需要一个动态网页,利用thymeleaf模板引擎(resource/templates)。

 
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <!--声明这不是html文件,而是一个thymeleaf模板-->
<head>
    <meta charset="UTF-8">
    <title>Teacher</title>
</head>
<body>
    <p th:text="${name}"></p>
    <p th:text="${age}"></p>
</body>
</html>

实现效果,程序已经设定初始值。

社交网站后端项目开发日记(一)

模板引擎会自动进行处理。

社交网站后端项目开发日记(一)

另一种方式:

 
@RequestMapping(path = "/school", method = RequestMethod.GET)
public String getSchool(Model model) {    //前端控制器会创建一个model
    model.addAttribute("name","大学");
    model.addAttribute("age",80);
    return "/demo/view";  //这里return的是view的路径
}

这两种方式是model和view的对象使用方式不同。效果还是一致的,第二种要更加简便一些。

社交网站后端项目开发日记(一)

响应JSON数据

异步请求可以理解为:当前网页不刷新,但是进行了数据交互,比如注册账号时,经常会检测用户名是否重复,这时候网页并没有刷新,但是访问了数据库进行查询。

 
//响应JSON数据(异步请求)
// Java对象传给浏览器:需要转为JS对象,这时候就可以通过JSON进行转化
// Java对象-> JSON字符串 -> JS对象等等
@RequestMapping(path = "/emp", method = RequestMethod.GET)
@ResponseBody
public Map<String,Object> getEmp() {  //自动会将map转为JSON字符串
    Map<String,Object> emp = new HashMap<>();
    emp.put("name","张三");
    emp.put("age",23);
    emp.put("salary",8000.00);
    return emp;
}

注意,这里并没有返回html。

这里只是简单介绍了响应方式,以后的博客中会介绍具体应用及代码。

社交网站后端项目开发日记(一)

3. MyBatis

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

社交网站后端项目开发日记(一)

mybatis文档

mybatis-spring文档

在maven项目pom.xml中添加依赖

 
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>8.0.25</version>
</dependency>
<dependency>
   <groupId>org.mybatis.spring.boot</groupId>
   <artifactId>mybatis-spring-boot-starter</artifactId>
   <version>2.2.0</version>
</dependency>

在SpringBoot框架下,只需要对application.properties进行配置,增加如下:

 
# DataSourceProperties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/community?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong
spring.datasource.username=root
spring.datasource.password=Ss6215615
#设置连接池,这个比较好
spring.datasource.type=com.zaxxer.hikari.HikariDataSource 
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000

# MybatisProperties
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.nowcoder.community.entity
mybatis.configuration.useGeneratedKeys=true
#自动匹配 a_b == aB
mybatis.configuration.mapUnderscoreToCamelCase=true

# logger,设置日志,方便在控制台查看信息
logging.level.com.nowcoder.community=debug

mybatis.type-aliases-package=com.nowcoder.community.entity

这行代码声明了SQL中记录在Java中的对象位置,根据SQL中的属性建立对象。

 
public class User {
    private int id;
    private String username;
    private String password;
    private String salt;
    private String email;
    private int type;
    private int status;
    private String activationCode;
    private String headerUrl;
    private Date createTime;
    //等等,还有set,get,toString

建立对象之后,在项目中的dao文件,建立UserMapper(对于User的操作)接口,Mybatis只需要通过xml文件对操作进行声明。

 
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.UserMapper">  <!-- 这里写UserMapper的全限定名 -->

    <sql id="selectFields">
        id, username, password, salt, email, type, status, activation_code, header_url, create_time
    </sql>

    <sql id="insertFields">
        username, password, salt, email, type, status, activation_code, header_url, create_time
    </sql>

    <select id="selectById" resultType="User">  <!-- 配置里进行过设置package,否则需要写全限定名 -->
        select <include refid="selectFields"></include>
        from user
        where id = #{id}  <!-- 这里的意思是要引用方法的参数 -->
    </select>

    <select id="selectByName" resultType="User">  <!-- 配置里进行过设置package,否则需要写全限定名 -->
        select <include refid="selectFields"></include>
        from user
        where username = #{username}  <!-- 这里的意思是要引用方法的参数 -->
    </select>

    <select id="selectByEmail" resultType="User">  <!-- 配置里进行过设置package,否则需要写全限定名 -->
        select id, username, password, salt, email, type, status, activation_code, header_url, create_time
        from user
        where email = #{email}  <!-- 这里的意思是要引用方法的参数 -->
    </select>

    <insert id="insertUser" parameterType="User" keyProperty="id">
        insert into user (<include refid="insertFields"></include>)
        values(#{username}, #{password}, #{salt}, #{email}, #{type}, #{status}, #{activationCode}, #{headerUrl}, #{createTime})
    </insert>

    <update id="updateStatus">
        update user set status = #{status} where id = #{id}
    </update>

    <update id="updateHeader">
        update user set header_url = #{headerUrl} where id = #{id}
    </update>

    <update id="updatePassword">
        update user set password = #{password} where id = #{id}
    </update>
</mapper>

这里建立了增删改的操作。

 
mybatis.mapper-locations=classpath:mapper/*.xml

根据已有的配置在resource文件夹中建立mapper目录和.xml文件。

由于.xml文件不会自动检测编译,建议在test中依次测试相应功能,以免出现错误。

4. 总结

本项目计划使用SpringBoot框架进行一个社交网站的开发,首先使用Spring Initializr对maven项目进行建立,之后对Spring的基本概念进行了一定阐述,IOC和AOP均没有讲到,将会在后续开放的Spring学习篇中,对SpringMVC的基本使用(GET,POST请求和响应数据)进行了代码示范,方便理解后续代码。另外,介绍了Mybatis的使用,以及公布了部分示范代码。

上手起来是非常快的,本系列的目的在于,重点讲解实战部分,理论部分将在其他博客中展开。

下篇博客将会对网站的登陆界面等如何实现进行阐述。

 

__EOF__

 
社交网站后端项目开发日记(一)
  • 本文作者: GaoYuan206
  • 本文链接: https://www.cnblogs.com/gaoyuan206/p/15013659.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐】一下。

相关文章: