Spring+Mybatis+SpringMVC后台与前台分页展示实例

  摘要:本文实现了一个后台由Spring+Mybatis+SpringMVC组成,分页采用PageHelper,前台展示使用bootstrap-paginator来显示效果的分页实例。整个项目由maven构成。这里主要讲了分页的实例,框架怎么搭建就不再说明,主要是在这里的基础上来增加分页功能的。注意,此文是在这个基础 Spring+Mybatis+SpringMVC+Maven+MySql搭建实例 之上来做分页的,建议文中看不懂的配置可以看看这里。

整个工程下载(旧版本,日志打印使用log4j,数据库配置放在properties文件)

新版本下载:https://github.com/appleappleapple/ssm_project (日志打印使用logback,数据库配置放在POM.XML)博主推荐使用新版本,而且这里详细说明了整个工程应用的框架,数据源配置,SQL语句等等!

重要的事情说三遍:请下新版本请下新版本请下新版本~

最后的结果如下:

环境:jdk1.6

     Tomcat 7.0

     Eclipse luna/windows 7    

一、后台PageHelper使用
PageHelper:https://github.com/pagehelper/Mybatis-PageHelper

1、引入jar包

	<!-- 添加分布插件的包pagehelper -->
	<dependency>
		<groupId>com.github.pagehelper</groupId>
		<artifactId>pagehelper</artifactId>
		<version>4.0.0</version>
	</dependency>

2.mybatis-config.xml中添加插件

这样子就引入进来了,接下来就是来开始分页功能的实现

3、mapper文件中添加如下一个方法:

<select id="selectUserByUserName" parameterType="java.lang.String" resultMap="BaseResultMap">
	SELECT *
	FROM t_user
	WHERE 1 = 1
	<if test="userName != null and userName !=''">
	    AND USER_NAME = #{userName,jdbcType=VARCHAR}
	</if>
	ORDER BY USER_ID
</select>

注意,这里的返回其实是一个list

<!--设置domain类和数据库中表的字段一一对应,注意数据库字段和domain类中的字段名称不致,此处一定要! -->
<resultMap id="BaseResultMap" type="com.lin.domain.User">
	<id column="USER_ID" property="userId" jdbcType="INTEGER" />
	<result column="USER_NAME" property="userName" jdbcType="CHAR" />
	<result column="USER_PASSWORD" property="userPassword" jdbcType="CHAR" />
	<result column="USER_EMAIL" property="userEmail" jdbcType="CHAR" />
</resultMap>

4、然后就是dao类

 /**
  * 
  * @author linbingwen
  * @since  2015年10月22日 
  * @param userName
  * @return
  */
 List<User> selectUserByUserName(@Param("userName") String userName);

这里一定的记得加@Param(“userName”)

接下来就可以在service层中添加分页查询的的接口了

5、接口类

/**
 * 
 * @author linbingwen
 * @since  2015年10月23日 
 * @param userName 查询条件,可为空
 * @param pageNo 查询条件,可为空,默认取1
 * @param pageSize 查询条件,可为空,默认取10
 * @return
 */
PagedResult<User> queryByPage(String userName,Integer pageNo,Integer pageSize);

6、实现类

public PagedResult<User> queryByPage(String userName,Integer pageNo,Integer pageSize ) {
	pageNo = pageNo == null?1:pageNo;
	pageSize = pageSize == null?10:pageSize;
	PageHelper.startPage(pageNo,pageSize);  //startPage是告诉拦截器说我要开始分页了。分页参数是这两个。
	return BeanUtil.toPagedResult(userDao.selectUserByUserName(userName));
}

这里就可以直接在返回里头使用了PageHelper,这里userDao.selectUserByUserName(userName)的返回是一个list

其中,PagedResult是我自己封装的一个分页结果类

package com.lin.util;

import java.util.List;

import com.lin.dto.BaseEntity;

/**

  • 功能概要:

  • @author linbingwen

  • @since 2015年10月23日
    */
    public class PagedResult extends BaseEntity {

    /serialVersionUID/
    private static final long serialVersionUID = 1L;

    private List dataList;//数据

    private long pageNo;//当前页

    private long pageSize;//条数

    private long total;//总条数

    private long pages;//总页面数目

    public List getDataList() {
    return dataList;
    }

    public void setDataList(List dataList) {
    this.dataList = dataList;
    }

    public long getPageNo() {
    return pageNo;
    }

    public void setPageNo(long pageNo) {
    this.pageNo = pageNo;
    }

    public long getPageSize() {
    return pageSize;
    }

    public void setPageSize(long pageSize) {
    this.pageSize = pageSize;
    }

    public long getTotal() {
    return total;
    }

    public void setTotal(long total) {
    this.total = total;
    }

    public long getPages() {
    return pages;
    }

    public void setPages(long pages) {
    this.pages = pages;
    }

}
这是它的基类

package com.lin.dto;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
*

  • 类说明:bean基类

  • 详细描述:

  • @author costin_law

  • @since 2014-5-5
    */
    public abstract class BaseEntity implements Serializable{
    private static final long serialVersionUID = 1L;

    private static Map<Class<?>,PropertyInfo[]> class2Props = new HashMap

package com.lin.util;

import java.util.List;

import com.github.pagehelper.Page;
import com.lin.util.PagedResult;

/**

  • 功能概要:
  • @author linbingwen
  • @since 2015年10月22日
    */

public class BeanUtil {

public static <T> PagedResult<T> toPagedResult(List<T> datas) {
    PagedResult<T> result = new PagedResult<T>();
    if (datas instanceof Page) {
        Page page = (Page) datas;
        result.setPageNo(page.getPageNum());
        result.setPageSize(page.getPageSize());
        result.setDataList(page.getResult());
        result.setTotal(page.getTotal());
        result.setPages(page.getPages());
    }
    else {
        result.setPageNo(1);
        result.setPageSize(datas.size());
        result.setDataList(datas);
        result.setTotal(datas.size());
    }

    return result;
}

}

7、这样就好了,可以跑单元测试了

/**
 * 分页测试
 * @author linbingwen
 * @since  2015年10月22日
 */
@Test
public void queryByPage(){
	 PagedResult<User>  pagedResult = userService.queryByPage(null,1,10);//null表示查全部	
	 logger.debug("查找结果" + pagedResult);
}

输出结果:

看不清的话看下面

查找结果{total:46,dataList:Page{pageNum=1, pageSize=10, startRow=0, endRow=10, total=46, pages=5, reasonable=false,

pageSizeZero=true},pageNo:1,pageSize:10,pages:5}

其中的dataList中存放的就是数据

打个断点看下就知道了:

二、前台展示分页结果
前台展示主要使用了bootstrap-paginator,这里的原理其实就是将上面查出来的结果,转换成json数据传给前台,然后前台再根据条数和分页数目、总目生成表格,同时每次点击对应的按钮都发送一个ajax请求到后台查询应对的数据,前台每次发送到后台都会包含分页数目、查询条件

1、Controller层的基类

这个基类主要实现了将数据转成json

引用到的jar包如下:

	<dependency>
		<groupId>net.sf.json-lib</groupId>
		<artifactId>json-lib</artifactId>
		<version>2.3</version>
		<classifier>jdk15</classifier>
	</dependency>

	<dependency>
		<groupId>org.springframework.data</groupId>
		<artifactId>spring-data-commons</artifactId>
		<version>1.6.1.RELEASE</version>
	</dependency>

	<dependency>
		<groupId>org.springframework.data</groupId>
		<artifactId>spring-data-jpa</artifactId>
		<version>1.4.1.RELEASE</version>
	</dependency>

	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>fastjson</artifactId>
		<version>1.1.34</version>
	</dependency>

基类如下:

package com.lin.controller;

import com.lin.common.HttpConstants;
import com.lin.json.JsonDateValueProcessor;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JsonConfig;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;

/**

  • Controller基类
    */
    public class BaseController {

    protected Logger logger = LoggerFactory.getLogger(this.getClass());

    protected final static String DATE_FORMATE = “yyyy-MM-dd”;

    /**

    • 返回服务端处理结果
    • @param obj 服务端输出对象
    • @return 输出处理结果给前段JSON格式数据
    • @author YANGHONGXIA
    • @since 2015-01-06
      */
      public String responseResult(Object obj){
      JSONObject jsonObj = null;
      if(obj != null){
      logger.info(“后端返回对象:{}”, obj);
      JsonConfig jsonConfig = new JsonConfig();
      jsonConfig.registerJsonValueProcessor(Date.class, new JsonDateValueProcessor());
      jsonObj = JSONObject.fromObject(obj, jsonConfig);
      logger.info(“后端返回数据:” + jsonObj);
      if(HttpConstants.SERVICE_RESPONSE_SUCCESS_CODE.equals(jsonObj.getString(HttpConstants.SERVICE_RESPONSE_RESULT_FLAG))){
      jsonObj.element(HttpConstants.RESPONSE_RESULT_FLAG_ISERROR, false);
      jsonObj.element(HttpConstants.SERVICE_RESPONSE_RESULT_MSG, “”);
      }else{
      jsonObj.element(HttpConstants.RESPONSE_RESULT_FLAG_ISERROR, true);
      String errMsg = jsonObj.getString(HttpConstants.SERVICE_RESPONSE_RESULT_MSG);
      jsonObj.element(HttpConstants.SERVICE_RESPONSE_RESULT_MSG, errMsg==null?HttpConstants.SERVICE_RESPONSE_NULL:errMsg);
      }
      }
      logger.info(“输出结果:{}”, jsonObj.toString());
      return jsonObj.toString();
      }

    /**

    • 返回成功
    • @param obj 输出对象
    • @return 输出成功的JSON格式数据
      */
      public String responseSuccess(Object obj){
      JSONObject jsonObj = null;
      if(obj != null){
      logger.info(“后端返回对象:{}”, obj);
      JsonConfig jsonConfig = new JsonConfig();
      jsonConfig.registerJsonValueProcessor(Date.class, new JsonDateValueProcessor());
      jsonObj = JSONObject.fromObject(obj, jsonConfig);
      logger.info(“后端返回数据:” + jsonObj);
      jsonObj.element(HttpConstants.RESPONSE_RESULT_FLAG_ISERROR, false);
      jsonObj.element(HttpConstants.SERVICE_RESPONSE_RESULT_MSG, “”);
      }
      logger.info(“输出结果:{}”, jsonObj.toString());
      return jsonObj.toString();
      }

    /**

    • 返回成功
    • @param obj 输出对象
    • @return 输出成功的JSON格式数据
      */
      public String responseArraySuccess(Object obj){
      JSONArray jsonObj = null;
      if(obj != null){
      logger.info(“后端返回对象:{}”, obj);
      JsonConfig jsonConfig = new JsonConfig();
      jsonConfig.registerJsonValueProcessor(Date.class, new JsonDateValueProcessor());
      jsonObj = JSONArray.fromObject(obj, jsonConfig);
      logger.info(“后端返回数据:” + jsonObj);
      }
      logger.info(“输出结果:{}”, jsonObj.toString());
      return jsonObj.toString();
      }

    /**

    • 返回成功
    • @param obj 输出对象
    • @return 输出成功的JSON格式数据
      */
      public String responseSuccess(Object obj, String msg){
      JSONObject jsonObj = null;
      if(obj != null){
      logger.info(“后端返回对象:{}”, obj);
      JsonConfig jsonConfig = new JsonConfig();
      jsonConfig.registerJsonValueProcessor(Date.class, new JsonDateValueProcessor());
      jsonObj = JSONObject.fromObject(obj, jsonConfig);
      logger.info(“后端返回数据:” + jsonObj);
      jsonObj.element(HttpConstants.RESPONSE_RESULT_FLAG_ISERROR, false);
      jsonObj.element(HttpConstants.SERVICE_RESPONSE_RESULT_MSG, msg);
      }
      logger.info(“输出结果:{}”, jsonObj.toString());
      return jsonObj.toString();
      }

    /**

    • 返回失败
    • @param errorMsg 错误信息
    • @return 输出失败的JSON格式数据
      */
      public String responseFail(String errorMsg){
      JSONObject jsonObj = new JSONObject();
      jsonObj.put(HttpConstants.RESPONSE_RESULT_FLAG_ISERROR, true);
      jsonObj.put(HttpConstants.SERVICE_RESPONSE_RESULT_MSG, errorMsg);
      logger.info(“输出结果:{}”, jsonObj.toString());
      return jsonObj.toString();
      }

}
上面用到的一些变量如下:

package com.lin.common;

public class HttpConstants {

public static final String SYSTEM_ERROR_MSG = "系统错误";

public static final String REQUEST_PARAMS_NULL = "请求参数为空";

public static final String SERVICE_RESPONSE_NULL = "服务端返回结果为空";

// 服务端返回成功的标志
public static final String SERVICE_RESPONSE_SUCCESS_CODE = "AMS00000";

// 服务端返回结果的标志
public static final String SERVICE_RESPONSE_RESULT_FLAG = "returnCode";

// 服务端返回结果失败的标志
public static final String SERVICE_RESPONSE_RESULT_MSG = "errorMsg";

// 返回给前段页面成功或失败的标志
public static final String RESPONSE_RESULT_FLAG_ISERROR = "isError";

// 执行删除操作
public static final String OPERATION_TYPE_DELETE = "D";

public static final String ENUM_PATH = "com.mucfc.msm.enumeration.";

}

引用一个包的内容如下:

package com.lin.json;

import net.sf.json.JsonConfig;
import net.sf.json.processors.JsonValueProcessor;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class JsonDateValueProcessor implements JsonValueProcessor {

/**
 * datePattern
 */
private String datePattern = "yyyy-MM-dd HH:mm:ss";

/**
 * JsonDateValueProcessor
 */
public JsonDateValueProcessor() {
    super();
}

/**
 * @param format
 */
public JsonDateValueProcessor(String format) {
    super();
    this.datePattern = format;
}

/**
 * @param value
 * @param jsonConfig
 * @return Object
 */
public Object processArrayValue(Object value, JsonConfig jsonConfig) {
    return process(value);
}

/**
 * @param key
 * @param value
 * @param jsonConfig
 * @return Object
 */
public Object processObjectValue(String key, Object value, JsonConfig jsonConfig) {
    return process(value);
}

/**
 * process
 *
 * @param value
 * @return
 */
private Object process(Object value) {
    try {
        if (value instanceof Date) {
            SimpleDateFormat sdf = new SimpleDateFormat(datePattern, Locale.UK);
            return sdf.format((Date) value);
        }
        return value == null ? "" : value.toString();
    } catch (Exception e) {
        return "";
    }

}

/**
 * @return the datePattern
 */
public String getDatePattern() {
    return datePattern;
}

/**
 * @param pDatePattern the datePattern to set
 */
public void setDatePattern(String pDatePattern) {
    datePattern = pDatePattern;
}

}

这里主要实现了能将list/map/set/数组等转换成josn,并传到前台‘

2、光这里写不行,还得配置springMVC中以json来传递数据,并配置自己的字符过滤器,要不然中文传到前台可能乱码,这里的配置比较复杂,大部分时间都花在这里,

这里我直接放spingMVC的配置:spring-mvc.xml

<?xml version="1.0" encoding="UTF-8"?>

<!-- 扫描controller(controller层注入) -->

<context:component-scan base-package=“com.lin.controller” use-default-filters=“false”>
<context:include-filter type=“annotation” expression=“org.springframework.stereotype.Controller”/>
<context:include-filter type=“annotation” expression=“org.springframework.web.bind.annotation.ControllerAdvice”/>
</context:component-scan>

   <!-- 会自动注册了validator  ConversionService  -->
<mvc:annotation-driven validator="validator" conversion-service="conversionService" content-negotiation-manager="contentNegotiationManager">
    <mvc:message-converters register-defaults="true">
        <!-- StringHttpMessageConverter编码为UTF-8,防止乱码 -->
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <constructor-arg value="UTF-8"/>
            <property name = "supportedMediaTypes">
                <list>
                    <bean class="org.springframework.http.MediaType">
                        <constructor-arg index="0" value="text"/>
                        <constructor-arg index="1" value="plain"/>
                        <constructor-arg index="2" value="UTF-8"/>
                    </bean>
                    <bean class="org.springframework.http.MediaType">
                        <constructor-arg index="0" value="*"/>
                        <constructor-arg index="1" value="*"/>
                        <constructor-arg index="2" value="UTF-8"/>
                    </bean>
                </list>
            </property>
        </bean>
        <!-- 避免IE执行AJAX时,返回JSON出现下载文件 -->
        <bean id="fastJsonHttpMessageConverter" class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
            <property name="supportedMediaTypes">
                <list>
                    <value>application/json;charset=UTF-8</value>
                </list>
            </property>
            <!--<property name="serializerFeature">-->
            <!--这个地方加上这个功能吧,能自己配置一些东西,比如时间的格式化,null输出""等等-->
            <!--</property>-->
        </bean>
    </mvc:message-converters>

    <mvc:argument-resolvers>
      <bean class="org.springframework.data.web.PageableHandlerMethodArgumentResolver" />
    </mvc:argument-resolvers>
</mvc:annotation-driven>


       <!-- 内容协商管理器  -->
<!--1、首先检查路径扩展名(如my.pdf);2、其次检查Parameter(如my?format=pdf);3、检查Accept Header-->
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <!-- 扩展名至mimeType的映射,即 /user.json => application/json -->
    <property name="favorPathExtension" value="true"/>
    <!-- 用于开启 /userinfo/123?format=json 的支持 -->
    <property name="favorParameter" value="true"/>
    <property name="parameterName" value="format"/>
    <!-- 是否忽略Accept Header -->
    <property name="ignoreAcceptHeader" value="false"/>

    <property name="mediaTypes"> <!--扩展名到MIME的映射;favorPathExtension, favorParameter是true时起作用  -->
        <value>
            json=application/json
            xml=application/xml
            html=text/html
        </value>
    </property>
    <!-- 默认的content type -->
    <property name="defaultContentType" value="text/html"/>
</bean>



<!-- 当在web.xml 中   DispatcherServlet使用 <url-pattern>/</url-pattern> 映射时,能映射静态资源 -->
<mvc:default-servlet-handler />  
<!-- 静态资源映射 -->
<mvc:resources mapping="/static/**" location="/WEB-INF/static/"/>
 <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
  p:prefix="/WEB-INF/view/" p:suffix=".jsp"/>
  
<!-- 这里设置静态的资源 -->

3、Spirng中也和配置:

<?xml version="1.0" encoding="UTF-8"?>

       	 <!-- 以下 validator  ConversionService 在使用 mvc:annotation-driven 会 自动注册-->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
</bean>
 
 <!-- 引入jdbc配置文件 -->  
 <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <list>
           <value>classpath:properties/*.properties</value>
            <!--要是有多个配置文件,只需在这里继续添加即可 -->
        </list>
    </property>
</bean>


	 <!-- 扫描注解Bean -->
<context:component-scan base-package="com.lin.service">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

<!-- 激活annotation功能 -->
<context:annotation-config />
<!-- 激活annotation功能 -->
<context:spring-configured />
<!-- 注解事务配置 -->

<!-- 类型转换及数据格式化 -->
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"/>	

<!-- 配置数据源 -->
<bean id="dataSource"
	class="org.springframework.jdbc.datasource.DriverManagerDataSource">
	<!-- 不使用properties来配置 -->
	<!-- <property name="driverClassName" value="com.mysql.jdbc.Driver" /> 
		<property name="url" value="jdbc:mysql://localhost:3306/learning" /> 
		<property name="username" value="root" /> 
		<property name="password" value="christmas258@" /> -->
   <!-- 使用properties来配置 -->
	<property name="driverClassName">
		<value>${jdbc_driverClassName}</value>
	</property>
	<property name="url">
		<value>${jdbc_url}</value>
	</property>
	<property name="username">
		<value>${jdbc_username}</value>
	</property>
	<property name="password">
		<value>${jdbc_password}</value>
	</property>
</bean>

<!-- 自动扫描了所有的XxxxMapper.xml对应的mapper接口文件,这样就不用一个一个手动配置Mpper的映射了,只要Mapper接口类和Mapper映射文件对应起来就可以了。 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<property name="basePackage"
		value="com.lin.dao" />
</bean>

<!-- 配置Mybatis的文件 ,mapperLocations配置**Mapper.xml文件位置,configLocation配置mybatis-config文件位置-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
	<property name="dataSource" ref="dataSource" />
    <property name="mapperLocations" value="classpath*:com/lin/mapper/**/*.xml"/>  
	<property name="configLocation" value="classpath:mybatis/mybatis-config.xml" />
	<!-- <property name="typeAliasesPackage" value="com.tiantian.ckeditor.model" 
		/> -->
</bean>

其中validator这个bean需要引用如下:

javax.validation validation-api 1.1.0.Final
	<dependency>
		<groupId>org.hibernate</groupId>
		<artifactId>hibernate-validator</artifactId>
		<version>5.0.1.Final</version>
	</dependency>

4、conroller层编写

package com.lin.controller;

import java.util.HashMap;
import java.util.Map;

import javax.annotation.Resource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import com.github.pagehelper.Page;
import com.lin.domain.User;
import com.lin.service.UserService;
import com.lin.util.PagedResult;

/**

  • 功能概要:UserController

  • @author linbingwen

  • @since 2015年9月28日
    */
    @Controller
    public class UserController extends BaseController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Resource
    private UserService userService;

    @RequestMapping("/")
    public ModelAndView getIndex(){
    ModelAndView mav = new ModelAndView(“index”);
    User user = userService.selectUserById(1);
    mav.addObject(“user”, user);
    return mav;
    }

    /**

    • 显示首页
    • @author linbingwen
    • @since 2015年10月23日
    • @return
      */
      @RequestMapping("/bootstrapTest1")
      public String bootStrapTest1(){
      return “bootstrap/bootstrapTest1”;
      }

    /**

    • 分页查询用户信息
    • @author linbingwen
    • @since 2015年10月23日
    • @param page
    • @return
      */
      @RequestMapping(value="/list.do", method= RequestMethod.POST)
      @ResponseBody
      public String list(Integer pageNumber,Integer pageSize ,String userName) {
      logger.info(“分页查询用户信息列表请求入参:pageNumber{},pageSize{}”, pageNumber,pageSize);
      try {
      PagedResult pageResult = userService.queryByPage(userName, pageNumber,pageSize);
      return responseSuccess(pageResult);
      } catch (Exception e) {
      return responseFail(e.getMessage());
      }
      }
      }

5、最后一步就是前台的页面了,这里可以先写页面再来写controller也可以的

<%@ page language=“java” contentType=“text/html; charset=UTF-8”
pageEncoding=“UTF-8”%>

Bootstrap分页实例
查询
查询用户结果
序号 用户名 密码 用户邮箱
注意引入的js文件,bootstrap-paginator需要引用bootstrap和jquery

6、最终运行结果

最后以web工程运行就可以了:

结果如下:

打印出来的一些日志:

后台返回给前台的就是json

本文转自:http://blog.csdn.net/evankaka

猜你喜欢

转载自blog.csdn.net/weixin_43242688/article/details/82792590