精华内容
下载资源
问答
  • :camera: VueJS响应式和可自定义的图像和视频库,轮播和灯箱,已针对移动和桌面Web浏览器进行了优化。 例 安装 CDN 推荐: : ,它将在发布到npm时立即反映最新版本。 您也可以在浏览npm软件包的源代码。 npm npm ...
  • 上文回顾上节 我们实现了仿jd的轮播广告以及商品分类的功能,并且讲解了不同的注入方式,本节我们将继续实现我们的电商主业务,商品信息的展示。需求分析首先,在我们开始本节编码之前,我们先来分析一下都有哪些...

    上文回顾

    上节 我们实现了仿jd的轮播广告以及商品分类的功能,并且讲解了不同的注入方式,本节我们将继续实现我们的电商主业务,商品信息的展示。

    需求分析

    首先,在我们开始本节编码之前,我们先来分析一下都有哪些地方会对商品进行展示,打开jd首页,鼠标下拉可以看到如下:

    555b8b6589b1ec79aec18e85d24d27d9.png

    可以看到,在大类型下查询了部分商品在首页进行展示(可以是最新的,也可以是网站推荐等等),然后点击任何一个分类,可以看到如下:

    4a3a6b84cb8ebcd62ec339acc77f74dd.png

    我们一般进到电商网站之后,最常用的一个功能就是搜索,搜索钢琴) 结果如下:

    9cbd939c42c79b6d485a894b759c7a6d.png

    选择任意一个商品点击,都可以进入到详情页面,这个是单个商品的信息展示。综上,我们可以知道,要实现一个电商平台的商品展示,最基本的包含:

    • 首页推荐/最新上架商品
    • 分类查询商品
    • 关键词搜索商品
    • 商品详情展示
    • ...

    接下来,我们就可以开始商品相关的业务开发了。

    首页商品列表|IndexProductList

    开发梳理

    我们首先来实现在首页展示的推荐商品列表,来看一下都需要展示哪些信息,以及如何进行展示。

    • 商品主键(product_id)
    • 展示图片(image_url)
    • 商品名称(product_name)
    • 商品价格(product_price)
    • 分类说明(description)
    • 分类名称(category_name)
    • 分类主键(category_id)
    • 其他...

    编码实现

    根据一级分类查询

    遵循开发顺序,自下而上,如果基础mapper解决不了,那么优先编写SQL mapper,因为我们需要在同一张表中根据parent_id递归的实现数据查询,当然我们这里使用的是表链接的方式实现。因此,common mapper无法满足我们的需求,需要自定义mapper实现。

    Custom Mapper实现

    和上节根据一级分类查询子分类一样,在项目mscx-shop-mapper中添加一个自定义实现接口com.liferunner.custom.ProductCustomMapper,然后在resourcesmappercustom路径下同步创建xml文件mapper/custom/ProductCustomMapper.xml,此时,因为我们在上节中已经配置了当前文件夹可以被容器扫描到,所以我们添加的新的mapper就会在启动时被扫描加载,代码如下:

    /**
     * ProductCustomMapper for : 自定义商品Mapper
     */
    public interface ProductCustomMapper {
    
        /***
         * 根据一级分类查询商品
         *
         * @param paramMap 传递一级分类(map传递多参数)
         * @return java.util.List<com.liferunner.dto.IndexProductDTO>
         */
        List<IndexProductDTO> getIndexProductDtoList(@Param("paramMap") Map<String, Integer> paramMap);
    }
    <?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.liferunner.custom.ProductCustomMapper">
        <resultMap id="IndexProductDTO" type="com.liferunner.dto.IndexProductDTO">
            <id column="rootCategoryId" property="rootCategoryId"/>
            <result column="rootCategoryName" property="rootCategoryName"/>
            <result column="slogan" property="slogan"/>
            <result column="categoryImage" property="categoryImage"/>
            <result column="bgColor" property="bgColor"/>
            <collection property="productItemList" ofType="com.liferunner.dto.IndexProductItemDTO">
                <id column="productId" property="productId"/>
                <result column="productName" property="productName"/>
                <result column="productMainImageUrl" property="productMainImageUrl"/>
                <result column="productCreateTime" property="productCreateTime"/>
            </collection>
        </resultMap>
        <select id="getIndexProductDtoList" resultMap="IndexProductDTO" parameterType="Map">
            SELECT
            c.id as rootCategoryId,
            c.name as rootCategoryName,
            c.slogan as slogan,
            c.category_image as categoryImage,
            c.bg_color as bgColor,
            p.id as productId,
            p.product_name as productName,
            pi.url as productMainImageUrl,
            p.created_time as productCreateTime
            FROM category c
            LEFT JOIN products p
            ON c.id = p.root_category_id
            LEFT JOIN products_img pi
            ON p.id = pi.product_id
            WHERE c.type = 1
            AND p.root_category_id = #{paramMap.rootCategoryId}
            AND pi.is_main = 1
            LIMIT 0,10;
        </select>
    </mapper>

    Service实现

    serviceproject 创建com.liferunner.service.IProductService接口以及其实现类com.liferunner.service.impl.ProductServiceImpl,添加查询方法如下:

    public interface IProductService {
    
        /**
         * 根据一级分类id获取首页推荐的商品list
         *
         * @param rootCategoryId 一级分类id
         * @return 商品list
         */
        List<IndexProductDTO> getIndexProductDtoList(Integer rootCategoryId);
        ...
    }
    
    ---
        
    @Slf4j
    @Service
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class ProductServiceImpl implements IProductService {
    
        // RequiredArgsConstructor 构造器注入
        private final ProductCustomMapper productCustomMapper;
    
        @Transactional(propagation = Propagation.SUPPORTS)
        @Override
        public List<IndexProductDTO> getIndexProductDtoList(Integer rootCategoryId) {
            log.info("====== ProductServiceImpl#getIndexProductDtoList(rootCategoryId) : {}=======", rootCategoryId);
            Map<String, Integer> map = new HashMap<>();
            map.put("rootCategoryId", rootCategoryId);
            val indexProductDtoList = this.productCustomMapper.getIndexProductDtoList(map);
            if (CollectionUtils.isEmpty(indexProductDtoList)) {
                log.warn("ProductServiceImpl#getIndexProductDtoList未查询到任何商品信息");
            }
            log.info("查询结果:{}", indexProductDtoList);
            return indexProductDtoList;
        }
    }

    Controller实现

    接着,在com.liferunner.api.controller.IndexController中实现对外暴露的查询接口:

    @RestController
    @RequestMapping("/index")
    @Api(value = "首页信息controller", tags = "首页信息接口API")
    @Slf4j
    public class IndexController {
           ...
        @Autowired
        private IProductService productService;
    
        @GetMapping("/rootCategorys")
        @ApiOperation(value = "查询一级分类", notes = "查询一级分类")
        public JsonResponse findAllRootCategorys() {
            log.info("============查询一级分类==============");
            val categoryResponseDTOS = this.categoryService.getAllRootCategorys();
            if (CollectionUtils.isEmpty(categoryResponseDTOS)) {
                log.info("============未查询到任何分类==============");
                return JsonResponse.ok(Collections.EMPTY_LIST);
            }
            log.info("============一级分类查询result:{}==============", categoryResponseDTOS);
            return JsonResponse.ok(categoryResponseDTOS);
        }
        ...
    }

    Test API

    编写完成之后,我们需要对我们的代码进行测试验证,还是通过使用RestService插件来实现,当然,大家也可以通过Postman来测试,结果如下:

    02f9d1561afdc1cf84238e6bd2aee4e2.png

    商品列表|ProductList

    如开文之初我们看到的京东商品列表一样,我们先分析一下在商品列表页面都需要哪些元素信息?

    开发梳理

    商品列表的展示按照我们之前的分析,总共分为2大类:

    • 选择商品分类之后,展示当前分类下所有商品
    • 输入搜索关键词后,展示当前搜索到相关的所有商品

    在这两类中展示的商品列表数据,除了数据来源不同以外,其他元素基本都保持一致,那么我们是否可以使用统一的接口来根据参数实现隔离呢? 理论上不存在问题,完全可以通过传参判断的方式进行数据回传,但是,在我们实现一些可预见的功能需求时,一定要给自己的开发预留后路,也就是我们常说的可拓展性,基于此,我们会分开实现各自的接口,以便于后期的扩展。接着来分析在列表页中我们需要展示的元素,首先因为需要分上述两种情况,因此我们需要在我们API设计的时候分别处理,针对于
    1.分类的商品列表展示,需要传入的参数有:

    • 分类id
    • 排序(在电商列表我们常见的几种排序(销量,价格等等))
    • 分页相关(因为我们不可能把数据库中所有的商品都取出来)
      • PageNumber(当前第几页)
      • PageSize(每页显示多少条数据)

    2.关键词查询商品列表,需要传入的参数有:

    • 关键词
    • 排序(在电商列表我们常见的几种排序(销量,价格等等))
    • 分页相关(因为我们不可能把数据库中所有的商品都取出来)
      • PageNumber(当前第几页)
      • PageSize(每页显示多少条数据)

    需要在页面展示的信息有:

    • 商品id(用于跳转商品详情使用)
    • 商品名称
    • 商品价格
    • 商品销量
    • 商品图片
    • 商品优惠
    • ...

    编码实现

    根据上面我们的分析,接下来开始我们的编码:

    根据商品分类查询

    根据我们的分析,肯定不会在一张表中把所有数据获取全,因此我们需要进行多表联查,故我们需要在自定义mapper中实现我们的功能查询.

    ResponseDTO 实现

    根据我们前面分析的前端需要展示的信息,我们来定义一个用于展示这些信息的对象com.liferunner.dto.SearchProductDTO,代码如下:

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class SearchProductDTO {
        private String productId;
        private String productName;
        private Integer sellCounts;
        private String imgUrl;
        private Integer priceDiscount;
        //商品优惠,我们直接计算之后返回优惠后价格
    }

    Custom Mapper 实现

    com.liferunner.custom.ProductCustomMapper.java中新增一个方法接口:

        List<SearchProductDTO> searchProductListByCategoryId(@Param("paramMap") Map<String, Object> paramMap);

    同时,在mapper/custom/ProductCustomMapper.xml中实现我们的查询方法:

    <select id="searchProductListByCategoryId" resultType="com.liferunner.dto.SearchProductDTO" parameterType="Map">
            SELECT
            p.id as productId,
            p.product_name as productName,
            p.sell_counts as sellCounts,
            pi.url as imgUrl,
            tp.priceDiscount
            FROM products p
            LEFT JOIN products_img pi
            ON p.id = pi.product_id
            LEFT JOIN
            (
            SELECT product_id, MIN(price_discount) as priceDiscount
            FROM products_spec
            GROUP BY product_id
            ) tp
            ON tp.product_id = p.id
            WHERE pi.is_main = 1
            AND p.category_id = #{paramMap.categoryId}
            ORDER BY
            <choose>
                <when test="paramMap.sortby != null and paramMap.sortby == 'sell'">
                    p.sell_counts DESC
                </when>
                <when test="paramMap.sortby != null and paramMap.sortby == 'price'">
                    tp.priceDiscount ASC
                </when>
                <otherwise>
                    p.created_time DESC
                </otherwise>
            </choose>
        </select>

    主要来说明一下这里的模块,以及为什么不使用if标签。
    在有的时候,我们并不希望所有的条件都同时生效,而只是想从多个选项中选择一个,但是在使用IF标签时,只要test中的表达式为 true,就会执行IF 标签中的条件。MyBatis 提供了 choose 元素。IF标签是与(and)的关系,而 choose 是或(or)的关系。
    它的选择是按照顺序自上而下,一旦有任何一个满足条件,则选择退出。

    Service 实现

    然后在servicecom.liferunner.service.IProductService中添加方法接口:

        /**
         * 根据商品分类查询商品列表
         *
         * @param categoryId 分类id
         * @param sortby     排序方式
         * @param pageNumber 当前页码
         * @param pageSize   每页展示多少条数据
         * @return 通用分页结果视图
         */
        CommonPagedResult searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize);

    在实现类com.liferunner.service.impl.ProductServiceImpl中,实现上述方法:

        // 方法重载
        @Override
        public CommonPagedResult searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize) {
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("categoryId", categoryId);
            paramMap.put("sortby", sortby);
            // mybatis-pagehelper
            PageHelper.startPage(pageNumber, pageSize);
            val searchProductDTOS = this.productCustomMapper.searchProductListByCategoryId(paramMap);
            // 获取mybatis插件中获取到信息
            PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
            // 封装为返回到前端分页组件可识别的视图
            val commonPagedResult = CommonPagedResult.builder()
                    .pageNumber(pageNumber)
                    .rows(searchProductDTOS)
                    .totalPage(pageInfo.getPages())
                    .records(pageInfo.getTotal())
                    .build();
            return commonPagedResult;
        }

    在这里,我们使用到了一个mybatis-pagehelper插件,会在下面的福利讲解中分解。

    Controller 实现

    继续在com.liferunner.api.controller.ProductController中添加对外暴露的接口API:

    @GetMapping("/searchByCategoryId")
        @ApiOperation(value = "查询商品信息列表", notes = "根据商品分类查询商品列表")
        public JsonResponse searchProductListByCategoryId(
            @ApiParam(name = "categoryId", value = "商品分类id", required = true, example = "0")
            @RequestParam Integer categoryId,
            @ApiParam(name = "sortby", value = "排序方式", required = false)
            @RequestParam String sortby,
            @ApiParam(name = "pageNumber", value = "当前页码", required = false, example = "1")
            @RequestParam Integer pageNumber,
            @ApiParam(name = "pageSize", value = "每页展示记录数", required = false, example = "10")
            @RequestParam Integer pageSize
        ) {
            if (null == categoryId || categoryId == 0) {
                return JsonResponse.errorMsg("分类id错误!");
            }
            if (null == pageNumber || 0 == pageNumber) {
                pageNumber = DEFAULT_PAGE_NUMBER;
            }
            if (null == pageSize || 0 == pageSize) {
                pageSize = DEFAULT_PAGE_SIZE;
            }
            log.info("============根据分类:{} 搜索列表==============", categoryId);
    
            val searchResult = this.productService.searchProductList(categoryId, sortby, pageNumber, pageSize);
            return JsonResponse.ok(searchResult);
        }

    因为我们的请求中,只会要求商品分类id是必填项,其余的调用方都可以不提供,但是如果不提供的话,我们系统就需要给定一些默认的参数来保证我们的系统正常稳定的运行,因此,我定义了com.liferunner.api.controller.BaseController,用于存储一些公共的配置信息。

    /**
     * BaseController for : controller 基类
     */
    @Controller
    public class BaseController {
        /**
         * 默认展示第1页
         */
        public final Integer DEFAULT_PAGE_NUMBER = 1;
        /**
         * 默认每页展示10条数据
         */
        public final Integer DEFAULT_PAGE_SIZE = 10;
    }

    Test API

    测试的参数分别是:categoryId : 51 ,sortby : price,pageNumber : 1,pageSize : 5

    56e1eb1b25e21018615da95e17de8d79.png

    可以看到,我们查询到7条数据,总页数totalPage为2,并且根据价格从小到大进行了排序,证明我们的编码是正确的。接下来,通过相同的代码逻辑,我们继续实现根据搜索关键词进行查询。

    根据关键词查询

    Response DTO 实现

    使用上面实现的com.liferunner.dto.SearchProductDTO.

    Custom Mapper 实现

    com.liferunner.custom.ProductCustomMapper中新增方法:

    List<SearchProductDTO> searchProductList(@Param("paramMap") Map<String, Object> paramMap);

    mapper/custom/ProductCustomMapper.xml中添加查询SQL:

    <select id="searchProductList" resultType="com.liferunner.dto.SearchProductDTO" parameterType="Map">
            SELECT
            p.id as productId,
            p.product_name as productName,
            p.sell_counts as sellCounts,
            pi.url as imgUrl,
            tp.priceDiscount
            FROM products p
            LEFT JOIN products_img pi
            ON p.id = pi.product_id
            LEFT JOIN
            (
            SELECT product_id, MIN(price_discount) as priceDiscount
            FROM products_spec
            GROUP BY product_id
            ) tp
            ON tp.product_id = p.id
            WHERE pi.is_main = 1
            <if test="paramMap.keyword != null and paramMap.keyword != ''">
                AND p.item_name LIKE "%${paramMap.keyword}%"
            </if>
            ORDER BY
            <choose>
                <when test="paramMap.sortby != null and paramMap.sortby == 'sell'">
                    p.sell_counts DESC
                </when>
                <when test="paramMap.sortby != null and paramMap.sortby == 'price'">
                    tp.priceDiscount ASC
                </when>
                <otherwise>
                    p.created_time DESC
                </otherwise>
            </choose>
        </select>

    Service 实现

    com.liferunner.service.IProductService中新增查询接口:

        /**
         * 查询商品列表
         *
         * @param keyword    查询关键词
         * @param sortby     排序方式
         * @param pageNumber 当前页码
         * @param pageSize   每页展示多少条数据
         * @return 通用分页结果视图
         */
        CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize);

    com.liferunner.service.impl.ProductServiceImpl实现上述接口方法:

        @Override
        public CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize) {
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("keyword", keyword);
            paramMap.put("sortby", sortby);
            // mybatis-pagehelper
            PageHelper.startPage(pageNumber, pageSize);
            val searchProductDTOS = this.productCustomMapper.searchProductList(paramMap);
            // 获取mybatis插件中获取到信息
            PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
            // 封装为返回到前端分页组件可识别的视图
            val commonPagedResult = CommonPagedResult.builder()
                    .pageNumber(pageNumber)
                    .rows(searchProductDTOS)
                    .totalPage(pageInfo.getPages())
                    .records(pageInfo.getTotal())
                    .build();
            return commonPagedResult;
        }

    上述方法和之前searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize) 唯一的区别就是它是肯定搜索关键词来进行数据查询,使用重载的目的是为了我们后续不同类型的业务扩展而考虑的。

    Controller 实现

    com.liferunner.api.controller.ProductController中添加关键词搜索API:

        @GetMapping("/search")
        @ApiOperation(value = "查询商品信息列表", notes = "查询商品信息列表")
        public JsonResponse searchProductList(
            @ApiParam(name = "keyword", value = "搜索关键词", required = true)
            @RequestParam String keyword,
            @ApiParam(name = "sortby", value = "排序方式", required = false)
            @RequestParam String sortby,
            @ApiParam(name = "pageNumber", value = "当前页码", required = false, example = "1")
            @RequestParam Integer pageNumber,
            @ApiParam(name = "pageSize", value = "每页展示记录数", required = false, example = "10")
            @RequestParam Integer pageSize
        ) {
            if (StringUtils.isBlank(keyword)) {
                return JsonResponse.errorMsg("搜索关键词不能为空!");
            }
            if (null == pageNumber || 0 == pageNumber) {
                pageNumber = DEFAULT_PAGE_NUMBER;
            }
            if (null == pageSize || 0 == pageSize) {
                pageSize = DEFAULT_PAGE_SIZE;
            }
            log.info("============根据关键词:{} 搜索列表==============", keyword);
    
            val searchResult = this.productService.searchProductList(keyword, sortby, pageNumber, pageSize);
            return JsonResponse.ok(searchResult);
        }

    Test API

    测试参数:keyword : 西凤,sortby : sell,pageNumber : 1,pageSize : 10

    f24d808616f91ef783132c17b973a15e.png


    根据销量排序正常,查询关键词正常,总条数32,每页10条,总共3页正常。

    福利讲解

    在本节编码实现中,我们使用到了一个通用的mybatis分页插件mybatis-pagehelper,接下来,我们来了解一下这个插件的基本情况。

    mybatis-pagehelper

    如果各位小伙伴使用过:MyBatis 分页插件 PageHelper, 那么对于这个就很容易理解了,它其实就是基于Executor 拦截器来实现的,当拦截到原始SQL之后,对SQL进行一次改造处理。
    我们来看看我们自己代码中的实现,根据springboot编码三部曲:

    1.添加依赖

            <!-- 引入mybatis-pagehelper 插件-->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>1.2.12</version>
            </dependency>

    有同学就要问了,为什么引入的这个依赖和我原来使用的不同?以前使用的是:

    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.1.10</version>
    </dependency>

    答案就在这里:依赖传送门

    bfc9a9e88bbdb88ac29e741f2d7e9d2a.png


    我们使用的是springboot进行的项目开发,既然使用的是springboot,那我们完全可以用到它的自动装配特性,作者帮我们实现了这么一个自动装配的jar,我们只需要参考示例来编写就ok了。

    2.改配置

    # mybatis 分页组件配置
    pagehelper:
      helperDialect: mysql #插件支持12种数据库,选择类型
      supportMethodsArguments: true

    3.改代码

    如下示例代码:

        @Override
        public CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize) {
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("keyword", keyword);
            paramMap.put("sortby", sortby);
            // mybatis-pagehelper
            PageHelper.startPage(pageNumber, pageSize);
            val searchProductDTOS = this.productCustomMapper.searchProductList(paramMap);
            // 获取mybatis插件中获取到信息
            PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
            // 封装为返回到前端分页组件可识别的视图
            val commonPagedResult = CommonPagedResult.builder()
                    .pageNumber(pageNumber)
                    .rows(searchProductDTOS)
                    .totalPage(pageInfo.getPages())
                    .records(pageInfo.getTotal())
                    .build();
            return commonPagedResult;
        }

    在我们查询数据库之前,我们引入了一句PageHelper.startPage(pageNumber, pageSize);,告诉mybatis我们要对查询进行分页处理,这个时候插件会启动一个拦截器com.github.pagehelper.PageInterceptor,针对所有的query进行拦截,添加自定义参数和添加查询数据总数。(后续我们会打印sql来证明。)

    当查询到结果之后,我们需要将我们查询到的结果通知给插件,也就是PageInfo pageInfo = new PageInfo<>(searchProductDTOS);com.github.pagehelper.PageInfo是对插件针对分页做的一个属性包装,具体可以查看属性传送门)。

    至此,我们的插件使用就已经结束了。但是为什么我们在后面又封装了一个对象来对外进行返回,而不是使用查询到的PageInfo呢?这是因为我们实际开发过程中,为了数据结构的一致性做的一次结构封装,你也可不实现该步骤,都是对结果没有任何影响的。

    SQL打印对比

    2019-11-21 12:04:21 INFO  ProductController:134 - ============根据关键词:西凤 搜索列表==============
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4ff449ba] was not registered for synchronization because synchronization is not active
    JDBC Connection [HikariProxyConnection@1980420239 wrapping com.mysql.cj.jdbc.ConnectionImpl@563b22b1] will not be managed by Spring
    ==>  Preparing: SELECT count(0) FROM products p LEFT JOIN products_img pi ON p.id = pi.product_id LEFT JOIN (SELECT product_id, MIN(price_discount) AS priceDiscount FROM products_spec GROUP BY product_id) tp ON tp.product_id = p.id WHERE pi.is_main = 1 AND p.product_name LIKE "%西凤%" 
    ==> Parameters: 
    <==    Columns: count(0)
    <==        Row: 32
    <==      Total: 1
    ==>  Preparing: SELECT p.id as productId, p.product_name as productName, p.sell_counts as sellCounts, pi.url as imgUrl, tp.priceDiscount FROM product p LEFT JOIN products_img pi ON p.id = pi.product_id LEFT JOIN ( SELECT product_id, MIN(price_discount) as priceDiscount FROM products_spec GROUP BY product_id ) tp ON tp.product_id = p.id WHERE pi.is_main = 1 AND p.product_name LIKE "%西凤%" ORDER BY p.sell_counts DESC LIMIT ? 
    ==> Parameters: 10(Integer)

    我们可以看到,我们的SQL中多了一个SELECT count(0),第二条SQL多了一个LIMIT参数,在代码中,我们很明确的知道,我们并没有显示的去搜索总数和查询条数,可以确定它就是插件帮我们实现的。

    源码下载

    Github 传送门
    Gitee 传送门

    下节预告

    下一节我们将继续开发商品详情展示以及商品评价业务,在过程中使用到的任何开发组件,我都会通过专门的一节来进行介绍的,兄弟们末慌!

    gogogo!

    奔跑的人生 | 博客园 | segmentfault | spring4all | csdn | 掘金 | OSChina | 简书 | 头条 | 知乎 | 51CTO

    展开全文
  • 在父工程下新建门户子模块protal—web 注意springmvc.xml文件中需要对dubbo进行设置 导入静态原型,controller实现类等文件。 搭建广告service模块 1、在父工程下引入两个子模块,分别是广告content的service接口...

    搭建门户网站子模块

    在父工程下新建门户子模块protal—web

    注意springmvc.xml文件中需要对dubbo进行设置
    

    在这里插入图片描述
    导入静态原型,controller实现类等文件。

    搭建广告service模块

    1、在父工程下引入两个子模块,分别是广告content的service接口,一个是广告content的service的Impl实现类。

    注意:在实现类的pom文件中,添加一个Tomcat的配置,
    

    在这里插入图片描述
    2、导入对应的content广告的接口和实现类。

    运营商(manager-web)广告列表的展示

    一、前端部分
    1、在content.html页面中引入js文件,这里需要分页的配置

    <script src="../plugins/angularjs/angular.min.js"></script>
    
    	<script type="text/javascript" src="../plugins/angularjs/pagination.js"></script>
    	<link rel="stylesheet" href="../plugins/angularjs/pagination.css">
    
    	<script src="../js/base_pagination.js"></script>
    	<script src="../js/service/contentService.js"></script>
    	<script src="../js/service/uploadService.js"></script>
    	<script src="../js/service/contentCategoryService.js"></script>
    	<script src="../js/controller/baseController.js"></script>
    	<script src="../js/controller/contentController.js"></script>
    
    

    2、在页面进行模型绑定和init方法的绑定

    注意:由于使用了分页的配置,所以不需要在init中写默认方法就可以实现自动搜索所有的列表信息。
    

    在这里插入图片描述
    在页面下方添加分页的配置
    在这里插入图片描述
    3、在js的controller和service中添加对应的方法

    注意:我们要显示所有的广告信息,需要在页面加载时查询所有的广告信息,并将对象放入list的模型对象中。
    

    流程分析:当页面改变时,baseController.js分页配置中的onchange方法执行。调用reloadList方法,reloadList方法调用search方法,执行contentController.js文件中的search方法
    在这里插入图片描述
    在这里插入图片描述
    二、后端部分
    常规操作。

    广告新增商品分类下拉列表的显示

    在点击新增上商品后,会弹出窗口,有一个下拉菜单可以选择广告所属的分类
    在这里插入图片描述
    一、前端部分
    1、页面的模型绑定,需要使用ng-options标签
    在这里插入图片描述
    2、init中添加默认方法findCategoryList在这里插入图片描述
    3、在js文件contentController.js中引入contentCategoryService
    在这里插入图片描述
    4、调用contentCategoryService中的findAll方法
    在这里插入图片描述

    二、后端部分
    引入对应的controller类后常规操作。

    广告新增的实现

    1、页面模型绑定

    仅需注意是否生效的状态勾选。在页面中勾选是true,没勾选是false。
    但是数据库中设置的输入长度为1。所有在前端需要进行值判断。勾选是1,没勾选是0;
    

    在这里插入图片描述
    2、保存按钮绑定方法

    广告修改

    页面的修改按钮调用findOne方法即可,传入id。
    在这里插入图片描述
    修改保存和新建保存使用的是同一个save方法,在controller会做id时候存在的判断,如 果存在代表是修改,不存在代表是新增。代码如下:

    	//保存 
    	$scope.save=function(){				
    		var serviceObject;//服务层对象  				
    		if($scope.entity.id!=null){//如果有ID
    			serviceObject=contentService.update( $scope.entity ); //修改  
    		}else{
    			serviceObject=contentService.add( $scope.entity  );//增加 
    		}				
    		serviceObject.success(
    			function(response){
    				if(response.success){
    					//重新查询 
    		        	$scope.reloadList();//重新加载
    				}else{
    					alert(response.message);
    				}
    			}		
    		);				
    	}
    

    广告删除

    一、前端部分
    1、在页面按钮绑定删除方法
    在这里插入图片描述
    2、在复选框添加项selectIds数组添加id的方法
    在这里插入图片描述
    3、在js文件controller定义删除方法
    在这里插入图片描述
    二、后端部分
    1、在impl层遍历数组删除所有广告对象
    在这里插入图片描述

    门户网站根据分类id获取分类下的所有广告

    一、前端部分
    1、页面模型绑定
    在这里插入图片描述
    注意图片中的其中一张的标签要添加active,表示默认第一张。
    使用了前端页面的三元运算符
    在这里插入图片描述
    2、由于不使用分页,所以在init中添加方法findByParentId,由于是轮播广告,所以传入固定值1。
    在这里插入图片描述
    3、在js文件中添加对应方法
    controller

    app.controller('contentController',function ($scope, contentService) {
    
        //根据分类id获取该分类下的所有列表数据
        $scope.findByCategoryId =function (id) {
            contentService.findByCategoryId(id).success(
                function (response) {
                    $scope.list = response;
                    //alert(JSON.stringify($scope.list)); //json对象转字符串
                }
            )
        }
    })
    

    service

    app.service('contentService',function ($http) {
    
        //查询该分类下的所有广告数据
        this.findByCategoryId = function (id) {
            return $http.get('content/findByCategoryId.do?id='+id);
        }
    })
    

    二、后端部分
    在模块中新建controller类,添加对应方法
    注意:为了使用ContentService,需要引入content-service模块

    package com.pinyougou.protal.controller;
    
    import com.alibaba.dubbo.config.annotation.Reference;
    import com.pinyougou.content.service.ContentService;
    import com.pinyougou.pojo.TbContent;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    
    @RestController
    @RequestMapping("/content")
    public class ContentController {
    
        @Reference
        private ContentService contentService;
    
        /**
         * 根据分类id获取该分类下的所有广告数据
         * @param id
         * @return
         */
        @RequestMapping("/findByCategoryId")
        public List<TbContent> findByCategoryId(Long id){
            return contentService.findByCategoryId(id);
        }
    
    }
    
    

    实现广告排序,状态等条件的设定

    在content的serviceImpl实现类中添加条件实现筛选
    在这里插入图片描述

    Linux安装redis

    1、将linux版的安装包赋值到soft目录下,解压到当前soft目录。
    2、进入解压后的文件夹中,执行安装指令,指令和指定的目录是:
    make install PREFIX=/usr/local/redis-cluster/redis
    3、打开安装好的文件目录,将解压出来的文件夹中的redis-conf文件复制进来。
    4、在安装好的目录下,打开bin文件夹,执行命令redis-server运行,出现如下图示,即代表开启完成。在这里插入图片描述

    广告列表保存到redis

    1、将redis-config.properties配置文件放入common模块中,引入common模块
    在这里插入图片描述
    redis-config.properties配置文件全文如下:(密码空指的是没有空格,不能加空格)

    # Redis settings 
    # server IP
    redis.host=192.168.25.128
    # server port
    redis.port=6379
    # server pass 
    redis.pass=
    # use dbIndex 
    redis.database=0
    # \u63A7\u5236\u4E00\u4E2Apool\u6700\u591A\u6709\u591A\u5C11\u4E2A\u72B6\u6001\u4E3Aidle(\u7A7A\u95F2\u7684)\u7684jedis\u5B9E\u4F8B 
    redis.maxIdle=300
    # \u8868\u793A\u5F53borrow(\u5F15\u5165)\u4E00\u4E2Ajedis\u5B9E\u4F8B\u65F6\uFF0C\u6700\u5927\u7684\u7B49\u5F85\u65F6\u95F4\uFF0C\u5982\u679C\u8D85\u8FC7\u7B49\u5F85\u65F6\u95F4(\u6BEB\u79D2)\uFF0C\u5219\u76F4\u63A5\u629B\u51FAJedisConnectionException\uFF1B  
    redis.maxWait=3000
    # \u5728borrow\u4E00\u4E2Ajedis\u5B9E\u4F8B\u65F6\uFF0C\u662F\u5426\u63D0\u524D\u8FDB\u884Cvalidate\u64CD\u4F5C\uFF1B\u5982\u679C\u4E3Atrue\uFF0C\u5219\u5F97\u5230\u7684jedis\u5B9E\u4F8B\u5747\u662F\u53EF\u7528\u7684  
    redis.testOnBorrow=true
    

    2、在需要使用redis的contentServiceImpl中添加applicationContext-redis.xml配置文件
    在这里插入图片描述
    applicationContext-redis.xml配置文件全文如下:

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" 
      xmlns:context="http://www.springframework.org/schema/context" 
      xmlns:mvc="http://www.springframework.org/schema/mvc" 
      xmlns:cache="http://www.springframework.org/schema/cache"
      xsi:schemaLocation="http://www.springframework.org/schema/beans   
                http://www.springframework.org/schema/beans/spring-beans.xsd   
                http://www.springframework.org/schema/context   
                http://www.springframework.org/schema/context/spring-context.xsd   
                http://www.springframework.org/schema/mvc   
                http://www.springframework.org/schema/mvc/spring-mvc.xsd 
                http://www.springframework.org/schema/cache  
                http://www.springframework.org/schema/cache/spring-cache.xsd">  
      
       <context:property-placeholder location="classpath*:properties/*.properties" />   
      
       <!-- redis 相关配置 --> 
       <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">  
         <property name="maxIdle" value="${redis.maxIdle}" />   
         <property name="maxWaitMillis" value="${redis.maxWait}" />  
         <property name="testOnBorrow" value="${redis.testOnBorrow}" />  
       </bean>  
      
       <bean id="JedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" 
           p:host-name="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}" p:pool-config-ref="poolConfig"/>  
       
       <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">  
        	<property name="connectionFactory" ref="JedisConnectionFactory" />  
       </bean>  
          
    </beans>  
    

    applicationContext-service.xml配置文件全文如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xmlns:mvc="http://www.springframework.org/schema/mvc"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
            http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    
        <dubbo:protocol name="dubbo" port="20882"></dubbo:protocol>
    	<dubbo:application name="myApplication-content-service"/>  
        <dubbo:registry address="zookeeper://192.168.25.128:2181"/>
        <dubbo:annotation package="com.myApplication.content.service.impl"/>
       
       
    </beans>
    

    3、在contentServiceImpl实现类中引入RedisTemplate对象。
    在这里插入图片描述
    4、在查找广告全部分类时,先从redis中判断,如果没有再去数据库中查找,将查到的结果再放到redis数据库中。
    在这里插入图片描述

    广告数据同步问题

    分析:当后台对广告进行了增删改,先放在redis中的数据不会改变,造成了数据不同步的现象。解决办法:在对应方法上,添加使数据同步的代码。

    增加:
    在这里插入图片描述
    删除:
    注意:需要先查找,再删除,否则都删除了,查不到了。
    在这里插入图片描述
    修改更新:
    注意:最为特殊

    注意:在修改时,可能会修改分类,当我们清空缓存时,清空的是新分类下的缓存。
    	新分类下是正确的。但是原先分类中没有删除,相当于原来分类中多了一个
    解决办法:在更新之前先查找出这个分类,在完成更新后将两个分类id进行比对,
    	如果不相同,需要将原来分类的id缓存也请清空
    

    在这里插入图片描述

    Redis常用命令集

    1)连接操作命令
    quit:关闭连接(connection)
    auth:简单密码认证
    help cmd: 查看cmd帮助,例如:help quit
    2)持久化
    save:将数据同步保存到磁盘
    bgsave:将数据异步保存到磁盘
    lastsave:返回上次成功将数据保存到磁盘的Unix时戳
    shundown:将数据同步保存到磁盘,然后关闭服务
    3)远程服务控制
    info:提供服务器的信息和统计
    monitor:实时转储收到的请求
    slaveof:改变复制策略设置
    config:在运行时配置Redis服务器
    4)对value操作的命令
    exists(key):确认一个key是否存在
    del(key):删除一个key
    type(key):返回值的类型
    keys(pattern):返回满足给定pattern的所有key
    randomkey:随机返回key空间的一个
    keyrename(oldname, newname):重命名key
    dbsize:返回当前数据库中key的数目
    expire:设定一个key的活动/生存时间(s)
    ttl:获得一个key的活动/生存时间
    select(index):按索引查询
    move(key, dbindex):移动当前数据库中的key到dbindex数据库
    flushdb:删除当前选择数据库中的所有key
    flushall:删除所有数据库中的所有key
    5)String
    set(key, value):给数据库中名称为key的string赋予值value
    get(key):返回数据库中名称为key的string的value
    getset(key, value):给名称为key的string赋予上一次的value
    mget(key1, key2,…, key N):返回库中多个string的value
    setnx(key, value):添加string,名称为key,值为value
    setex(key, time, value):向库中添加string,设定过期时间time
    mset(key N, value N):批量设置多个string的值
    msetnx(key N, value N):如果所有名称为key i的string都不存在
    incr(key):名称为key的string增1操作
    incrby(key, integer):名称为key的string增加integer
    decr(key):名称为key的string减1操作
    decrby(key, integer):名称为key的string减少integer
    append(key, value):名称为key的string的值附加value
    substr(key, start, end):返回名称为key的string的value的子串
    6)List
    rpush(key, value):在名称为key的list尾添加一个值为value的元素
    lpush(key, value):在名称为key的list头添加一个值为value的 元素
    llen(key):返回名称为key的list的长度
    lrange(key, start, end):返回名称为key的list中start至end之间的元素
    ltrim(key, start, end):截取名称为key的list
    lindex(key, index):返回名称为key的list中index位置的元素
    lset(key, index, value):给名称为key的list中index位置的元素赋值
    lrem(key, count, value):删除count个key的list中值为value的元素
    lpop(key):返回并删除名称为key的list中的首元素
    rpop(key):返回并删除名称为key的list中的尾元素
    blpop(key1, key2,… key N, timeout):lpop命令的block版本。
    brpop(key1, key2,… key N, timeout):rpop的block版本。
    rpoplpush(srckey, dstkey):返回并删除名称为srckey的list的尾元素,并将该元素添加到名称为dstkey的list的头部
    7)Set
    sadd(key, member):向名称为key的set中添加元素member
    srem(key, member) :删除名称为key的set中的元素member
    spop(key) :随机返回并删除名称为key的set中一个元素
    smove(srckey, dstkey, member) :移到集合元素
    scard(key) :返回名称为key的set的基数
    sismember(key, member) :member是否是名称为key的set的元素
    sinter(key1, key2,…key N) :求交集
    sinterstore(dstkey, (keys)) :求交集并将交集保存到dstkey的集合
    sunion(key1, (keys)) :求并集
    sunionstore(dstkey, (keys)) :求并集并将并集保存到dstkey的集合
    sdiff(key1, (keys)) :求差集
    sdiffstore(dstkey, (keys)) :求差集并将差集保存到dstkey的集合
    smembers(key) :返回名称为key的set的所有元素
    srandmember(key) :随机返回名称为key的set的一个元素
    8)Hash
    hset(key, field, value):向名称为key的hash中添加元素field
    hget(key, field):返回名称为key的hash中field对应的value
    hmget(key, (fields)):返回名称为key的hash中field i对应的value
    hmset(key, (fields)):向名称为key的hash中添加元素field
    hincrby(key, field, integer):将名称为key的hash中field的value增加integer
    hexists(key, field):名称为key的hash中是否存在键为field的域
    hdel(key, field):删除名称为key的hash中键为field的域
    hlen(key):返回名称为key的hash中元素个数
    hkeys(key):返回名称为key的hash中所有键
    hvals(key):返回名称为key的hash中所有键对应的value
    hgetall(key):返回名称为key的hash中所有的键(field)及其对应的value

    展开全文
  • 上节 我们实现了仿jd的轮播广告以及商品分类的功能,并且讲解了不同的注入方式,本节我们将继续实现我们的电商主业务,商品信息的展示。 需求分析 首先,在我们开始本节编码之前,我们先来分析一下都有哪些地方会对...

    上文回顾

    上节 我们实现了仿jd的轮播广告以及商品分类的功能,并且讲解了不同的注入方式,本节我们将继续实现我们的电商主业务,商品信息的展示。

    需求分析

    首先,在我们开始本节编码之前,我们先来分析一下都有哪些地方会对商品进行展示,打开jd首页,鼠标下拉可以看到如下:
    首页商品列表示例

    可以看到,在大类型下查询了部分商品在首页进行展示(可以是最新的,也可以是网站推荐等等),然后点击任何一个分类,可以看到如下:
    分类商品列表示例

    我们一般进到电商网站之后,最常用的一个功能就是搜索,搜索钢琴 结果如下:
    搜索查询结果示例

    选择任意一个商品点击,都可以进入到详情页面,这个是单个商品的信息展示。
    综上,我们可以知道,要实现一个电商平台的商品展示,最基本的包含:

    • 首页推荐/最新上架商品
    • 分类查询商品
    • 关键词搜索商品
    • 商品详情展示

    接下来,我们就可以开始商品相关的业务开发了。

    首页商品列表|IndexProductList

    开发梳理

    我们首先来实现在首页展示的推荐商品列表,来看一下都需要展示哪些信息,以及如何进行展示。

    • 商品主键(product_id)
    • 展示图片(image_url)
    • 商品名称(product_name)
    • 商品价格(product_price)
    • 分类说明(description)
    • 分类名称(category_name)
    • 分类主键(category_id)
    • 其他…

    编码实现

    根据一级分类查询

    遵循开发顺序,自下而上,如果基础mapper解决不了,那么优先编写SQL mapper,因为我们需要在同一张表中根据parent_id递归的实现数据查询,当然我们这里使用的是表链接的方式实现。因此,common mapper无法满足我们的需求,需要自定义mapper实现。

    Custom Mapper实现

    上节根据一级分类查询子分类一样,在项目mscx-shop-mapper中添加一个自定义实现接口com.liferunner.custom.ProductCustomMapper,然后在resources\mapper\custom路径下同步创建xml文件mapper/custom/ProductCustomMapper.xml,此时,因为我们在上节中已经配置了当前文件夹可以被容器扫描到,所以我们添加的新的mapper就会在启动时被扫描加载,代码如下:

    /**
     * ProductCustomMapper for : 自定义商品Mapper
     */
    public interface ProductCustomMapper {
    
        /***
         * 根据一级分类查询商品
         *
         * @param paramMap 传递一级分类(map传递多参数)
         * @return java.util.List<com.liferunner.dto.IndexProductDTO>
         */
        List<IndexProductDTO> getIndexProductDtoList(@Param("paramMap") Map<String, Integer> paramMap);
    }
    
    <?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.liferunner.custom.ProductCustomMapper">
        <resultMap id="IndexProductDTO" type="com.liferunner.dto.IndexProductDTO">
            <id column="rootCategoryId" property="rootCategoryId"/>
            <result column="rootCategoryName" property="rootCategoryName"/>
            <result column="slogan" property="slogan"/>
            <result column="categoryImage" property="categoryImage"/>
            <result column="bgColor" property="bgColor"/>
            <collection property="productItemList" ofType="com.liferunner.dto.IndexProductItemDTO">
                <id column="productId" property="productId"/>
                <result column="productName" property="productName"/>
                <result column="productMainImageUrl" property="productMainImageUrl"/>
                <result column="productCreateTime" property="productCreateTime"/>
            </collection>
        </resultMap>
        <select id="getIndexProductDtoList" resultMap="IndexProductDTO" parameterType="Map">
            SELECT
            c.id as rootCategoryId,
            c.name as rootCategoryName,
            c.slogan as slogan,
            c.category_image as categoryImage,
            c.bg_color as bgColor,
            p.id as productId,
            p.product_name as productName,
            pi.url as productMainImageUrl,
            p.created_time as productCreateTime
            FROM category c
            LEFT JOIN products p
            ON c.id = p.root_category_id
            LEFT JOIN products_img pi
            ON p.id = pi.product_id
            WHERE c.type = 1
            AND p.root_category_id = #{paramMap.rootCategoryId}
            AND pi.is_main = 1
            LIMIT 0,10;
        </select>
    </mapper>
    

    Service实现

    serviceproject 创建com.liferunner.service.IProductService接口以及其实现类com.liferunner.service.impl.ProductServiceImpl,添加查询方法如下:

    public interface IProductService {
    
        /**
         * 根据一级分类id获取首页推荐的商品list
         *
         * @param rootCategoryId 一级分类id
         * @return 商品list
         */
        List<IndexProductDTO> getIndexProductDtoList(Integer rootCategoryId);
    	...
    }
    
    ---
        
    @Slf4j
    @Service
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class ProductServiceImpl implements IProductService {
    
        // RequiredArgsConstructor 构造器注入
        private final ProductCustomMapper productCustomMapper;
    
        @Transactional(propagation = Propagation.SUPPORTS)
        @Override
        public List<IndexProductDTO> getIndexProductDtoList(Integer rootCategoryId) {
            log.info("====== ProductServiceImpl#getIndexProductDtoList(rootCategoryId) : {}=======", rootCategoryId);
            Map<String, Integer> map = new HashMap<>();
            map.put("rootCategoryId", rootCategoryId);
            val indexProductDtoList = this.productCustomMapper.getIndexProductDtoList(map);
            if (CollectionUtils.isEmpty(indexProductDtoList)) {
                log.warn("ProductServiceImpl#getIndexProductDtoList未查询到任何商品信息");
            }
            log.info("查询结果:{}", indexProductDtoList);
            return indexProductDtoList;
        }
    }
    

    Controller实现

    接着,在com.liferunner.api.controller.IndexController中实现对外暴露的查询接口:

    @RestController
    @RequestMapping("/index")
    @Api(value = "首页信息controller", tags = "首页信息接口API")
    @Slf4j
    public class IndexController {
       	...
        @Autowired
        private IProductService productService;
    
        @GetMapping("/rootCategorys")
        @ApiOperation(value = "查询一级分类", notes = "查询一级分类")
        public JsonResponse findAllRootCategorys() {
            log.info("============查询一级分类==============");
            val categoryResponseDTOS = this.categoryService.getAllRootCategorys();
            if (CollectionUtils.isEmpty(categoryResponseDTOS)) {
                log.info("============未查询到任何分类==============");
                return JsonResponse.ok(Collections.EMPTY_LIST);
            }
            log.info("============一级分类查询result:{}==============", categoryResponseDTOS);
            return JsonResponse.ok(categoryResponseDTOS);
        }
        ...
    }
    

    Test API

    编写完成之后,我们需要对我们的代码进行测试验证,还是通过使用RestService插件来实现,当然,大家也可以通过Postman来测试,结果如下:
    根据一级分类查询商品列表

    商品列表|ProductList

    如开文之初我们看到的京东商品列表一样,我们先分析一下在商品列表页面都需要哪些元素信息?

    开发梳理

    商品列表的展示按照我们之前的分析,总共分为2大类:

    • 选择商品分类之后,展示当前分类下所有商品
    • 输入搜索关键词后,展示当前搜索到相关的所有商品

    在这两类中展示的商品列表数据,除了数据来源不同以外,其他元素基本都保持一致,那么我们是否可以使用统一的接口来根据参数实现隔离呢? 理论上不存在问题,完全可以通过传参判断的方式进行数据回传,但是,在我们实现一些可预见的功能需求时,一定要给自己的开发预留后路,也就是我们常说的可拓展性,基于此,我们会分开实现各自的接口,以便于后期的扩展。
    接着来分析在列表页中我们需要展示的元素,首先因为需要分上述两种情况,因此我们需要在我们API设计的时候分别处理,针对于
    1.分类的商品列表展示,需要传入的参数有:

    • 分类id
    • 排序(在电商列表我们常见的几种排序(销量,价格等等))
    • 分页相关(因为我们不可能把数据库中所有的商品都取出来)
      • PageNumber(当前第几页)
      • PageSize(每页显示多少条数据)

    2.关键词查询商品列表,需要传入的参数有:

    • 关键词
    • 排序(在电商列表我们常见的几种排序(销量,价格等等))
    • 分页相关(因为我们不可能把数据库中所有的商品都取出来)
      • PageNumber(当前第几页)
      • PageSize(每页显示多少条数据)

    需要在页面展示的信息有:

    • 商品id(用于跳转商品详情使用)
    • 商品名称
    • 商品价格
    • 商品销量
    • 商品图片
    • 商品优惠

    编码实现

    根据上面我们的分析,接下来开始我们的编码:

    根据商品分类查询

    根据我们的分析,肯定不会在一张表中把所有数据获取全,因此我们需要进行多表联查,故我们需要在自定义mapper中实现我们的功能查询.

    ResponseDTO 实现

    根据我们前面分析的前端需要展示的信息,我们来定义一个用于展示这些信息的对象com.liferunner.dto.SearchProductDTO,代码如下:

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class SearchProductDTO {
        private String productId;
        private String productName;
        private Integer sellCounts;
        private String imgUrl;
        private Integer priceDiscount;
        //商品优惠,我们直接计算之后返回优惠后价格
    }
    

    Custom Mapper 实现

    com.liferunner.custom.ProductCustomMapper.java中新增一个方法接口:

        List<SearchProductDTO> searchProductListByCategoryId(@Param("paramMap") Map<String, Object> paramMap);
    

    同时,在mapper/custom/ProductCustomMapper.xml中实现我们的查询方法:

    <select id="searchProductListByCategoryId" resultType="com.liferunner.dto.SearchProductDTO" parameterType="Map">
            SELECT
            p.id as productId,
            p.product_name as productName,
            p.sell_counts as sellCounts,
            pi.url as imgUrl,
            tp.priceDiscount
            FROM products p
            LEFT JOIN products_img pi
            ON p.id = pi.product_id
            LEFT JOIN
            (
            SELECT product_id, MIN(price_discount) as priceDiscount
            FROM products_spec
            GROUP BY product_id
            ) tp
            ON tp.product_id = p.id
            WHERE pi.is_main = 1
            AND p.category_id = #{paramMap.categoryId}
            ORDER BY
            <choose>
                <when test="paramMap.sortby != null and paramMap.sortby == 'sell'">
                    p.sell_counts DESC
                </when>
                <when test="paramMap.sortby != null and paramMap.sortby == 'price'">
                    tp.priceDiscount ASC
                </when>
                <otherwise>
                    p.created_time DESC
                </otherwise>
            </choose>
        </select>
    

    主要来说明一下这里的<choose>模块,以及为什么不使用if标签。
    在有的时候,我们并不希望所有的条件都同时生效,而只是想从多个选项中选择一个,但是在使用IF标签时,只要test中的表达式为 true,就会执行IF 标签中的条件。MyBatis 提供了 choose 元素。IF标签是与(and)的关系,而 choose 是或(or)的关系。
    它的选择是按照顺序自上而下,一旦有任何一个满足条件,则选择退出。

    Service 实现

    然后在servicecom.liferunner.service.IProductService中添加方法接口:

        /**
         * 根据商品分类查询商品列表
         *
         * @param categoryId 分类id
         * @param sortby     排序方式
         * @param pageNumber 当前页码
         * @param pageSize   每页展示多少条数据
         * @return 通用分页结果视图
         */
        CommonPagedResult searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize);
    

    在实现类com.liferunner.service.impl.ProductServiceImpl中,实现上述方法:

        // 方法重载
        @Override
        public CommonPagedResult searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize) {
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("categoryId", categoryId);
            paramMap.put("sortby", sortby);
            // mybatis-pagehelper
            PageHelper.startPage(pageNumber, pageSize);
            val searchProductDTOS = this.productCustomMapper.searchProductListByCategoryId(paramMap);
            // 获取mybatis插件中获取到信息
            PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
            // 封装为返回到前端分页组件可识别的视图
            val commonPagedResult = CommonPagedResult.builder()
                    .pageNumber(pageNumber)
                    .rows(searchProductDTOS)
                    .totalPage(pageInfo.getPages())
                    .records(pageInfo.getTotal())
                    .build();
            return commonPagedResult;
        }
    

    在这里,我们使用到了一个mybatis-pagehelper插件,会在下面的福利讲解中分解。

    Controller 实现

    继续在com.liferunner.api.controller.ProductController中添加对外暴露的接口API:

    @GetMapping("/searchByCategoryId")
        @ApiOperation(value = "查询商品信息列表", notes = "根据商品分类查询商品列表")
        public JsonResponse searchProductListByCategoryId(
            @ApiParam(name = "categoryId", value = "商品分类id", required = true, example = "0")
            @RequestParam Integer categoryId,
            @ApiParam(name = "sortby", value = "排序方式", required = false)
            @RequestParam String sortby,
            @ApiParam(name = "pageNumber", value = "当前页码", required = false, example = "1")
            @RequestParam Integer pageNumber,
            @ApiParam(name = "pageSize", value = "每页展示记录数", required = false, example = "10")
            @RequestParam Integer pageSize
        ) {
            if (null == categoryId || categoryId == 0) {
                return JsonResponse.errorMsg("分类id错误!");
            }
            if (null == pageNumber || 0 == pageNumber) {
                pageNumber = DEFAULT_PAGE_NUMBER;
            }
            if (null == pageSize || 0 == pageSize) {
                pageSize = DEFAULT_PAGE_SIZE;
            }
            log.info("============根据分类:{} 搜索列表==============", categoryId);
    
            val searchResult = this.productService.searchProductList(categoryId, sortby, pageNumber, pageSize);
            return JsonResponse.ok(searchResult);
        }
    

    因为我们的请求中,只会要求商品分类id是必填项,其余的调用方都可以不提供,但是如果不提供的话,我们系统就需要给定一些默认的参数来保证我们的系统正常稳定的运行,因此,我定义了com.liferunner.api.controller.BaseController,用于存储一些公共的配置信息。

    /**
     * BaseController for : controller 基类
     */
    @Controller
    public class BaseController {
        /**
         * 默认展示第1页
         */
        public final Integer DEFAULT_PAGE_NUMBER = 1;
        /**
         * 默认每页展示10条数据
         */
        public final Integer DEFAULT_PAGE_SIZE = 10;
    }
    

    Test API

    测试的参数分别是:categoryId : 51 ,sortby : price,pageNumber : 1,pageSize : 5

    根据分类id查询

    可以看到,我们查询到7条数据,总页数totalPage为2,并且根据价格从小到大进行了排序,证明我们的编码是正确的。接下来,通过相同的代码逻辑,我们继续实现根据搜索关键词进行查询。

    根据关键词查询

    Response DTO 实现

    使用上面实现的com.liferunner.dto.SearchProductDTO.

    Custom Mapper 实现

    com.liferunner.custom.ProductCustomMapper中新增方法:

    List<SearchProductDTO> searchProductList(@Param("paramMap") Map<String, Object> paramMap);
    

    mapper/custom/ProductCustomMapper.xml中添加查询SQL:

    <select id="searchProductList" resultType="com.liferunner.dto.SearchProductDTO" parameterType="Map">
            SELECT
            p.id as productId,
            p.product_name as productName,
            p.sell_counts as sellCounts,
            pi.url as imgUrl,
            tp.priceDiscount
            FROM products p
            LEFT JOIN products_img pi
            ON p.id = pi.product_id
            LEFT JOIN
            (
            SELECT product_id, MIN(price_discount) as priceDiscount
            FROM products_spec
            GROUP BY product_id
            ) tp
            ON tp.product_id = p.id
            WHERE pi.is_main = 1
            <if test="paramMap.keyword != null and paramMap.keyword != ''">
                AND p.item_name LIKE "%${paramMap.keyword}%"
            </if>
            ORDER BY
            <choose>
                <when test="paramMap.sortby != null and paramMap.sortby == 'sell'">
                    p.sell_counts DESC
                </when>
                <when test="paramMap.sortby != null and paramMap.sortby == 'price'">
                    tp.priceDiscount ASC
                </when>
                <otherwise>
                    p.created_time DESC
                </otherwise>
            </choose>
        </select>
    

    Service 实现

    com.liferunner.service.IProductService中新增查询接口:

        /**
         * 查询商品列表
         *
         * @param keyword    查询关键词
         * @param sortby     排序方式
         * @param pageNumber 当前页码
         * @param pageSize   每页展示多少条数据
         * @return 通用分页结果视图
         */
        CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize);
    

    com.liferunner.service.impl.ProductServiceImpl实现上述接口方法:

        @Override
        public CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize) {
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("keyword", keyword);
            paramMap.put("sortby", sortby);
            // mybatis-pagehelper
            PageHelper.startPage(pageNumber, pageSize);
            val searchProductDTOS = this.productCustomMapper.searchProductList(paramMap);
            // 获取mybatis插件中获取到信息
            PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
            // 封装为返回到前端分页组件可识别的视图
            val commonPagedResult = CommonPagedResult.builder()
                    .pageNumber(pageNumber)
                    .rows(searchProductDTOS)
                    .totalPage(pageInfo.getPages())
                    .records(pageInfo.getTotal())
                    .build();
            return commonPagedResult;
        }
    

    上述方法和之前searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize)唯一的区别就是它是肯定搜索关键词来进行数据查询,使用重载的目的是为了我们后续不同类型的业务扩展而考虑的。

    Controller 实现

    com.liferunner.api.controller.ProductController中添加关键词搜索API:

        @GetMapping("/search")
        @ApiOperation(value = "查询商品信息列表", notes = "查询商品信息列表")
        public JsonResponse searchProductList(
            @ApiParam(name = "keyword", value = "搜索关键词", required = true)
            @RequestParam String keyword,
            @ApiParam(name = "sortby", value = "排序方式", required = false)
            @RequestParam String sortby,
            @ApiParam(name = "pageNumber", value = "当前页码", required = false, example = "1")
            @RequestParam Integer pageNumber,
            @ApiParam(name = "pageSize", value = "每页展示记录数", required = false, example = "10")
            @RequestParam Integer pageSize
        ) {
            if (StringUtils.isBlank(keyword)) {
                return JsonResponse.errorMsg("搜索关键词不能为空!");
            }
            if (null == pageNumber || 0 == pageNumber) {
                pageNumber = DEFAULT_PAGE_NUMBER;
            }
            if (null == pageSize || 0 == pageSize) {
                pageSize = DEFAULT_PAGE_SIZE;
            }
            log.info("============根据关键词:{} 搜索列表==============", keyword);
    
            val searchResult = this.productService.searchProductList(keyword, sortby, pageNumber, pageSize);
            return JsonResponse.ok(searchResult);
        }
    

    Test API

    测试参数:keyword : 西凤,sortby : sell,pageNumber : 1,pageSize : 10
    测试关键词结果
    根据销量排序正常,查询关键词正常,总条数32,每页10条,总共3页正常。

    福利讲解

    在本节编码实现中,我们使用到了一个通用的mybatis分页插件mybatis-pagehelper,接下来,我们来了解一下这个插件的基本情况。

    mybatis-pagehelper

    如果各位小伙伴使用过:MyBatis 分页插件 PageHelper, 那么对于这个就很容易理解了,它其实就是基于Executor 拦截器来实现的,当拦截到原始SQL之后,对SQL进行一次改造处理。
    我们来看看我们自己代码中的实现,根据springboot编码三部曲:

    1.添加依赖

            <!-- 引入mybatis-pagehelper 插件-->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>1.2.12</version>
            </dependency>
    

    有同学就要问了,为什么引入的这个依赖和我原来使用的不同?以前使用的是:

    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.1.10</version>
    </dependency>
    

    答案就在这里:依赖传送门
    spring-boot-pagehelper
    我们使用的是springboot进行的项目开发,既然使用的是springboot,那我们完全可以用到它的自动装配特性,作者帮我们实现了这么一个自动装配的jar,我们只需要参考示例来编写就ok了。

    2.改配置

    # mybatis 分页组件配置
    pagehelper:
      helperDialect: mysql #插件支持12种数据库,选择类型
      supportMethodsArguments: true
    

    3.改代码

    如下示例代码:

        @Override
        public CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize) {
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("keyword", keyword);
            paramMap.put("sortby", sortby);
            // mybatis-pagehelper
            PageHelper.startPage(pageNumber, pageSize);
            val searchProductDTOS = this.productCustomMapper.searchProductList(paramMap);
            // 获取mybatis插件中获取到信息
            PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
            // 封装为返回到前端分页组件可识别的视图
            val commonPagedResult = CommonPagedResult.builder()
                    .pageNumber(pageNumber)
                    .rows(searchProductDTOS)
                    .totalPage(pageInfo.getPages())
                    .records(pageInfo.getTotal())
                    .build();
            return commonPagedResult;
        }
    

    在我们查询数据库之前,我们引入了一句PageHelper.startPage(pageNumber, pageSize);,告诉mybatis我们要对查询进行分页处理,这个时候插件会启动一个拦截器com.github.pagehelper.PageInterceptor,针对所有的query进行拦截,添加自定义参数和添加查询数据总数。(后续我们会打印sql来证明。)

    当查询到结果之后,我们需要将我们查询到的结果通知给插件,也就是PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);com.github.pagehelper.PageInfo是对插件针对分页做的一个属性包装,具体可以查看属性传送门)。

    至此,我们的插件使用就已经结束了。但是为什么我们在后面又封装了一个对象来对外进行返回,而不是使用查询到的PageInfo呢?这是因为我们实际开发过程中,为了数据结构的一致性做的一次结构封装,你也可不实现该步骤,都是对结果没有任何影响的。

    SQL打印对比

    2019-11-21 12:04:21 INFO  ProductController:134 - ============根据关键词:西凤 搜索列表==============
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4ff449ba] was not registered for synchronization because synchronization is not active
    JDBC Connection [HikariProxyConnection@1980420239 wrapping com.mysql.cj.jdbc.ConnectionImpl@563b22b1] will not be managed by Spring
    ==>  Preparing: SELECT count(0) FROM products p LEFT JOIN products_img pi ON p.id = pi.product_id LEFT JOIN (SELECT product_id, MIN(price_discount) AS priceDiscount FROM products_spec GROUP BY product_id) tp ON tp.product_id = p.id WHERE pi.is_main = 1 AND p.product_name LIKE "%西凤%" 
    ==> Parameters: 
    <==    Columns: count(0)
    <==        Row: 32
    <==      Total: 1
    ==>  Preparing: SELECT p.id as productId, p.product_name as productName, p.sell_counts as sellCounts, pi.url as imgUrl, tp.priceDiscount FROM product p LEFT JOIN products_img pi ON p.id = pi.product_id LEFT JOIN ( SELECT product_id, MIN(price_discount) as priceDiscount FROM products_spec GROUP BY product_id ) tp ON tp.product_id = p.id WHERE pi.is_main = 1 AND p.product_name LIKE "%西凤%" ORDER BY p.sell_counts DESC LIMIT ? 
    ==> Parameters: 10(Integer)
    

    我们可以看到,我们的SQL中多了一个SELECT count(0),第二条SQL多了一个LIMIT参数,在代码中,我们很明确的知道,我们并没有显示的去搜索总数和查询条数,可以确定它就是插件帮我们实现的。

    源码下载

    Github 传送门
    Gitee 传送门

    下节预告

    下一节我们将继续开发商品详情展示以及商品评价业务,在过程中使用到的任何开发组件,我都会通过专门的一节来进行介绍的,兄弟们末慌!

    gogogo!

    奔跑的人生 | 博客园 | segmentfault | spring4all | csdn | 掘金 | OSChina | 简书 | 头条 | 知乎 | 51CTO

    展开全文
  • 基于java web的博客系统技术点:主界面轮播博客列表查看博客用户注册后台管理个人信息管理 技术点: 后端:java+springboot+mybatis 前端: layui 数据库:mysql 开发工具:idea 主界面轮播 博客列表 查看博客 ...

    技术点:

    后端:java+springboot+mybatis
    前端: layui
    数据库:mysql
    开发工具:idea

    主界面轮播

    在这里插入图片描述

    博客列表

    在这里插入图片描述

    查看博客

    在这里插入图片描述

    用户注册

    在这里插入图片描述

    后台管理

    在这里插入图片描述

    个人信息管理

    在这里插入图片描述

    展开全文
  • 图片结构是一个列表型结构,所以主体用ul标签; 使用css绝对定位将图片重叠在同一个位置; 轮播图切换的状态使用修饰符(modifier); 轮播图的切换动画使用 css transition。 -. 优化1:采用依赖注入降低代码的耦合...
  • 很棒的工具,服务,扩展和精美书签列表 :exclamation_mark: 我列出的用于编程和Web开发的出色Web工具,服务,扩展等 :OK_hand: 目录 1.测试人员和验证人员 :bug: 2.工具 :hammer: 3.代码生成器 :laptop_computer: ...
  • 大多数应用程序都使用常见的通用用户界面模式,例如列表,菜单,对话框,轮播等。 这样的模式可以有效地实现并打包为Web组件。 它们的模块化性质使您可以轻松地将Web组件合并到Web应用程序中,并且它们的标准定义可...
  • 主要内容详情页跳转、详情页面及路由、轮播图组件使用、封装Tab标签页、房屋评价滚动加载更多、收藏功能登录验证和收藏功能。 第一节 详情页跳转搜索列表修改:给列表元素项添加Link标签,to属性添加跳转路由名称...
  • ┃ ┣━第4节 轮播图效果实现 ┃ ┣━第8节 移动端插件的使用 ┃ ┣━第2节 首页动态效果 ┃ ┣━第5节 zepto.js ┃ ┣━第3节 移动端的touch事件 ┃ ┣━第1节 首页布局 ┣━14 响应式开发基础知识 ┃ ┣━第2节 ...
  • WebComponents 欢迎Star 欢迎ISSUE,您的鼓励会是我写作的动力! 在使用的过程中,如果对插件有新的功能需求,欢迎 fork 发起 pull request 。如果您没有时间更新,请建立 ISSUE 我会考虑后加入该功能! 插件...
  • Vue2.5学习笔记电商项目实战创建项目 vue-mart选择一个合适的UI库V1.0版本App.vueLogin.vuemain.jsrouter.jsstore.jshttp拦截器vue.config.js作业V2.0版本tab导航轮播图商品列表购物车UI展示小结V3.0版本Header组件...
  • 用户端(A类页面)大概功能有:商品分类列表、首页全文检索(全网站搜索商品、分类等),首页图片轮播,首页分类下的商品推荐,加入购物车,购物车选中结算,分类下的商品列表、商品详情展示等等。 管理端(B类页面)...
  • [ ] 组件UI样式:按钮组、字体图标、下拉菜单、输入框组、导航组、面包屑、分页、标签、轮播、弹出框、列表、多媒体、警告 [ ] 响应式布局:布局、结构、样式、媒体、javascript响应式 [ ] 第三方插件:插件管理 ...
  • 1.轮播图2.首页列表页面跳转页面跳转 <navigator url='xxx/xxx?id=123'></navigator> 页面跳转 小程序api跳转页面 wx.naxxx 给当前的元素添加事件 在事件里面跳转页面组件data-属性 传递数值 事件的参数...
  • 2018mui教程 mui实战视频教程 web移动端开发教程+源码 课程简介: 结合MUI框架完成HTML5移动端混合应用开发(微信实战)。 本课程将介绍如何使用HTML5完成移动端混合应用开发,课程以移动端框架MUI为基础,以微信...
  • 首页:显示分类、轮播图、热门推荐。 商品列表:展示后台设置的商品,分页。 搜索商品:通过关键字搜索相关商品。 商品详情:展示商品的详情图文、标题、价格和发布时间,可以加入购物车。 添加购物车:商品详情...
  • 组件UI样式:按钮组、字体图标、下拉菜单、输入框组、导航组、面包屑、分页、标签、轮播、弹出框、列表、多媒体、警告 响应式布局:布局、结构、样式、媒体、javascript响应式 第三方插件:插件管理 jQuery、zepto...
  • Ignite UI for Angular旨在使开发人员能够针对Google的Angular框架针对现代桌面浏览器,移动体验和渐进式Web应用程序(PWA)构建最现代,高性能HTML5和JavaScript应用程序。 您可以在文件夹下找到源文件,包括示例...
  • DlnaWenUI采用java写的一个dlna的web端,可以通过浏览器查看minidlna服务下的文件和视频。如何发布服务可通过tomcat或jetty进行发布,工程采用maven管理。功能访问首页是文件列表(带分页),视频播放器采用HTML5...
  • grid网格布局基础UI样式:元素reset、按钮、图片、菜单、表单组件UI样式:按钮组、字体图标、下拉菜单、输入框组、导航组、面包屑、分页、标签、轮播、弹出框、列表、多媒体、警告响应式布局:布局、结构、样式、...
  • │ └─ CategoryContent.vue 展示分类页面左部分列表和右部分商品数据 ├─ detail 商品详情页 │ ├─ Detail.vue │ └─ childCompos │ ├─ DetailBaseInfo.vue 展示商品详情页轮播图下方的基本商品数据 ...
  • ego-portal-web 系统需求分析 1.1 搭建 Redis 集群环境 ⚫ 实现 redis3 主 3 从集群搭建 1.2 实现门户首页商品类目显示 ⚫ 加载商品类目 ⚫ 实现商品类目缓存 1.3 实现每户首页大广告轮播 ⚫ 加载大广告列表 ...
  • 第4章 推荐页面开发

    2019-06-17 08:33:28
    包括 jsonp 原理介绍和 Promise 封装、轮播图组件开发、歌单接口数据分析和抓取、axios 介绍和后端接口代理、歌单列表组件开发和数据应用、scroll 组件的抽象和应用、vue-lazyloader 懒加载插件的介绍和应用、...
  • 如下图所示,可以看到内容显示是分组显示的,广告位轮播图是一组,轮播图下面那两张图片是一组,右边的"促销"、"公告"是一组,但是它与轮播图又有所不同,不同之处在于它下面分了两个小组,每个小组下面有标题列表,...
  • 前端需要展示一个列表如首页的轮播图后台需要能够控制列表数据的排序。 需求 自动规则 + 运营干预干预包括降低排名提升排名设定位置和新增item 实现 在Web后台管理系统中列表形式的数据排序功能是很常见的需求。要...
  • 实现轮播广告图根据后台设置的广告列表动态产生 1.工程搭建 创建 war 模块 pinyougou-portal-web ,此工程为网站前台的入口,参照其它 war 模块编写配置文件。 2.前端代码 1.拷贝资源:资源文件夹中 “前台页面”...
  • 导语: ...本文的目录一,数据库结构1,用户2,文章3,轮播图4,评论二,后台的sql执行语句1,admin.js,管理员的接口(1)创建新文章(2)更新文章2,article.js,用户的文章接口(1)获取文章列表

空空如也

空空如也

1 2 3 4 5 6
收藏数 106
精华内容 42
关键字:

web轮播列表