目录

一、商品详情

1.1 商品详情页服务

1.1.1 创建module

1.1.2 pom依赖

1.1.3 编写启动类

1.1.4 application.yml

1.1.5 页面模板

1.2 页面跳转

1.2.1 修改页面跳转路径

1.2.2 nginx反向代理

1.2.3 编写跳转Controller

1.2.4 测试

1.3 封装模型数据

1.3.1 商品微服务提供接口

1.3.2 创建FeignClient

1.3.3 封装数据模型

1.3.4 页面测试数据

1.4 渲染面包屑

1.5 渲染商品列表

1.5.1 副标题

1.5.2 渲染规格属性列表

1.5.3 规格属性筛选

1.5.4 确定sku

1.5.5 渲染sku列表

1.6 商品详情

1.6.1 属性列表

1.6.2 商品详情

1.7 规格包装

1.7.1 规格参数

1.7.2 包装列表

1.8 售后服务


一、商品详情

1.1 商品详情页服务

商品详情浏览量比较大,并发高,所以独立开发一个微服务,用来展示商品详情

1.1.1 创建module

商品的详情页服务

1.1.2 pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.goods</groupId>
    <artifactId>leyou-goods-web</artifactId>

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

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

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

        <dependency>
            <groupId>com.leyou.item.interface</groupId>
            <artifactId>leyou-item-interface</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

1.1.3 编写启动类

package com.leyou.goods;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @Author: 98050
 * Time: 2018-10-17 11:10
 * Feature: 商品详情微服务启动器,开启fegin功能
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LyGoodsWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(LyGoodsWebApplication.class,args);
    }
}

1.1.4 application.yml

server:
  port: 8084
spring:
  application:
    name: goods-page-service
  thymeleaf:
    cache: false
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
    registry-fetch-interval-seconds: 5
  instance:
    instance-id: ${spring.application.name}:${server.port}
    prefer-ip-address: true  #当你获取host时,返回的不是主机名,而是ip
    ip-address: 127.0.0.1
    lease-expiration-duration-in-seconds: 10 #10秒不发送九过期
    lease-renewal-interval-in-seconds: 5 #每隔5秒发一次心跳

1.1.5 页面模板

从leyou-portal中复制item.html模板到当前项目resource目录下的templates中:

1.2 页面跳转

1.2.1 修改页面跳转路径

首先需要修改搜索结果页的商品地址,目前所有商品的地址都是:http://www.leyou.com/item.html

应该跳转到对应的商品的详情页才对。

那么问题来了:商品详情页是一个SKU?还是多个SKU的集合?

通过详情页的预览,知道它是多个SKU的集合,即SPU。

所以,页面跳转时,应该携带SPU的id信息。

例如:http://www.leyou.com/item/2314123.html

这里就采用了路径占位符的方式来传递spu的id,打开search.html,修改其中的商品路径:

刷新页面:

1.2.2 nginx反向代理

接下来,要把这个地址指向我们刚刚创建的服务:leyou-goods-web,其端口为8084

需要在nginx.conf中添加一段配置:

把以/item开头的请求,代理到8084端口。

1.2.3 编写跳转Controller

leyou-goods-web中编写controller,接收请求,并跳转到商品详情页:

package com.leyou.goods.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @Author: 98050
 * Time: 2018-10-17 16:06
 * Feature: 商品详情页面跳转
 */
@Controller
@RequestMapping("item")
public class GoodsController {
    
    @GetMapping("{id}.html")
    public String toItemPage(@PathVariable("id")Long id){
        return "item";
    }
}

1.2.4 测试

启动leyou-goods-page,点击搜索页面商品,看是能够正常跳转

1.3 封装模型数据

首先分析一下,在这个页面中需要哪些数据

已知的条件是传递来的spu的id,然后需要根据spu的id查询到下面的数据:

  • spu信息

  • spu的详情

  • spu下的所有sku

  • 品牌

  • 商品三级分类

  • 商品规格参数、规格参数组

1.3.1 商品微服务提供接口

以上所需数据中,查询spu的接口目前还没有,需要在商品微服务中提供这个接口:

GoodsApi

1.3.2 创建FeignClient

复制leyou-search微服务中的FeignClient

1.3.3 封装数据模型

创建一个GoodsService,在里面来封装数据模型。

这里要查询的数据:

  • SPU

  • SpuDetail

  • SKU集合

  • 商品分类

    • 这里值需要分类的id和name就够了,因此我们查询到以后自己需要封装数据

  • 品牌

  • 规格组

    • 查询规格组的时候,把规格组下所有的参数也一并查出

因为在页面展示规格时,需要按组展示:

 

  • sku的特有规格参数

GoodsService代码

package com.leyou.service.impl;

import com.fasterxml.jackson.core.type.TypeReference;
import com.leyou.client.*;
import com.leyou.item.bo.SpuBo;
import com.leyou.item.pojo.*;
import com.leyou.service.GoodsService;
import com.leyou.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * @Author: 98050
 * Time: 2018-10-17 19:40
 * Feature:商品详情页信息
 */
@Service
public class GoodsServiceImpl implements GoodsService {

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private CategoryClient categoryClient;

    @Override
    public Map<String, Object> loadModel(Long spuId) {

        //查询商品信息
        SpuBo spuBo = this.goodsClient.queryGoodsById(spuId);

        //查询商品详情
        SpuDetail spuDetail = spuBo.getSpuDetail();

        //查询skus
        List<Sku> skuList = spuBo.getSkus();

        //查询分类信息
        List<Long> ids = new ArrayList<>();
        ids.add(spuBo.getCid1());
        ids.add(spuBo.getCid2());
        ids.add(spuBo.getCid3());
        List<Category> categoryList = this.categoryClient.queryCategoryByIds(ids).getBody();

        //查询品牌信息
        Brand brand = this.brandClient.queryBrandByIds(Collections.singletonList(spuBo.getBrandId())).get(0);


        /**
         * 对于规格属性的处理需要注意以下几点:
         *      1. 所有规格都保存为id和name形式
         *      2. 规格对应的值保存为id和value形式
         *      3. 都是map形式
         *      4. 将特有规格参数单独抽取
         */

        //获取所有规格参数,然后封装成为id和name形式的数据
        String allSpecJson = spuDetail.getSpecifications();
        List<Map<String,Object>> allSpecs = JsonUtils.nativeRead(allSpecJson, new TypeReference<List<Map<String, Object>>>() {
        });
        Map<Integer,String> specName = new HashMap<>();
        Map<Integer,Object> specValue = new HashMap<>();
        this.getAllSpecifications(allSpecs,specName,specValue);


        //获取特有规格参数
        String specTJson = spuDetail.getSpecTemplate();
        Map<String,String[]> specs = JsonUtils.nativeRead(specTJson, new TypeReference<Map<String, String[]>>() {
        });
        Map<Integer,String> specialParamName = new HashMap<>();
        Map<Integer,String[]> specialParamValue = new HashMap<>();
        this.getSpecialSpec(specs,specName,specValue,specialParamName,specialParamValue);

        //按照组构造规格参数
        List<Map<String,Object>> groups = this.getGroupsSpec(allSpecs,specName,specValue);
        groups.forEach(System.out::println);


        Map<String,Object> map = new HashMap<>();
        map.put("spu",spuBo);
        map.put("spuDetail",spuDetail);
        map.put("skus",skuList);
        map.put("brand",brand);
        map.put("categories",categoryList);
        map.put("specName",specName);
        map.put("specValue",specValue);
        map.put("groups",groups);
        map.put("specialParamName",specialParamName);
        map.put("specialParamValue",specialParamValue);

        return map;
    }

    private List<Map<String, Object>> getGroupsSpec(List<Map<String, Object>> allSpecs, Map<Integer, String> specName, Map<Integer, Object> specValue) {
        List<Map<String, Object>> groups = new ArrayList<>();
        int i = 0;
        int j = 0;
        for (Map<String,Object> spec :allSpecs){
            List<Map<String, Object>> params = (List<Map<String, Object>>) spec.get("params");
            List<Map<String,Object>> temp = new ArrayList<>();
            for (Map<String,Object> param :params) {
                for (Map.Entry<Integer, String> entry : specName.entrySet()) {
                    if (entry.getValue().equals(param.get("k").toString())) {
                        String value = specValue.get(entry.getKey()) != null ? specValue.get(entry.getKey()).toString() : "无";
                        Map<String, Object> temp3 = new HashMap<>(16);
                        temp3.put("id", ++j);
                        temp3.put("name", entry.getValue());
                        temp3.put("value", value);
                        temp.add(temp3);
                    }
                }
            }
            Map<String,Object> temp2 = new HashMap<>(16);
            temp2.put("params",temp);
            temp2.put("id",++i);
            temp2.put("name",spec.get("group"));
            groups.add(temp2);
        }
        return groups;
    }

    private void getSpecialSpec(Map<String, String[]> specs, Map<Integer, String> specName, Map<Integer, Object> specValue, Map<Integer, String> specialParamName, Map<Integer, String[]> specialParamValue) {
        if (specs != null) {
            for (Map.Entry<String, String[]> entry : specs.entrySet()) {
                String key = entry.getKey();
                for (Map.Entry<Integer,String> e : specName.entrySet()) {
                    if (e.getValue().equals(key)){
                        specialParamName.put(e.getKey(),e.getValue());
                        //因为是放在数组里面,所以要先去除两个方括号,然后再以逗号分割成数组
                        String  s = specValue.get(e.getKey()).toString();
                        String result = StringUtils.substring(s,1,s.length()-1);
                        specialParamValue.put(e.getKey(), result.split(","));
                    }
                }
            }
        }
    }

    private void getAllSpecifications(List<Map<String, Object>> allSpecs, Map<Integer, String> specName, Map<Integer, Object> specValue) {
        String k = "k";
        String v = "v";
        String unit = "unit";
        String numerical = "numerical";
        String options ="options";
        int i = 0;
        if (allSpecs != null){
            for (Map<String,Object> s : allSpecs){
                List<Map<String, Object>> params = (List<Map<String, Object>>) s.get("params");
                for (Map<String,Object> param :params){
                    String result;
                    if (param.get(v) == null){
                        result = "无";
                    }else{
                        result = param.get(v).toString();
                    }
                    if (param.containsKey(numerical) && (boolean) param.get(numerical)) {
                        if (result.contains(".")){
                            Double d = Double.valueOf(result);
                            if (d.intValue() == d){
                                result = d.intValue()+"";
                            }
                        }
                        i++;
                        specName.put(i,param.get(k).toString());
                        specValue.put(i,result+param.get(unit).toString());
                    } else if (param.containsKey(options)){
                        i++;
                        specName.put(i,param.get(k).toString());
                        specValue.put(i,param.get(options));
                    }else {
                        i++;
                        specName.put(i,param.get(k).toString());
                        specValue.put(i,param.get(v));
                    }
                }
            }
        }
    }

}

函数解析:

因为在搭建后台管理的时候,为了满足业务需求,新建了一个spuBo的业务对象,在这里可以继续使用,然后再做扩展。

  • 查询商品信息,根据spuId查询业务对象spuBo
  • 查询商品详情,可以在spuBo中获取。

商品详情包括以下内容,在渲染页面时会用到

  • 查询sku集合
  • 查询商品分类信息
  • 查询品牌信息
  • 获取所有规格参数,然后编号,然后进行拆分,分别保存为id和name,id和value,具体参考下面的数据结构。

specName                                                                                                     specValue

                                                                               

  • 获取特有属性参数

和上面的数据结构一样,只是把对应的特有属性抽取出来。如何知道哪些属性是特有属性?它们保存在spuDetail中的specTemplate里,所以将其转化为map类型,然后根据对应的键值在specName和specValue中提取即可。

specialParamName

specialParamValue

Controller代码

package com.leyou.controller;

import com.leyou.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.Map;

/**
 * @Author: 98050
 * Time: 2018-10-17 16:06
 * Feature: 商品详情页面跳转
 */
@Controller
@RequestMapping("item")
public class GoodsController {

    @Autowired
    private GoodsService goodsService;

    @GetMapping("{id}.html")
    public String toItemPage(Model model,@PathVariable("id")Long id){
        Map<String,Object> modelMap =  this.goodsService.loadModel(id);
        model.addAllAttributes(modelMap);
        return "item";
    }
}

1.3.4 页面测试数据

1.4 渲染面包屑

在商品展示页的顶部,有一个商品分类、品牌、标题的面包屑

其数据有3部分:

  • 商品分类

  • 商品品牌

  • spu标题

我们的模型中都有,所以直接渲染即可:

1.5 渲染商品列表

预期效果如下:

这个部分需要渲染的数据有5块:

  • sku图片

  • sku标题

  • 副标题

  • sku价格

  • 特有规格属性列表

其中,sku 的图片、标题、价格,都必须在用户选中一个具体sku后,才能渲染。而特有规格属性列表可以在spuDetail中查询到。而副标题则是在spu中,直接可以在页面渲染

因此,先对特有规格属性列表进行渲染。等用户选择一个sku,再通过js对其它sku属性渲染

1.5.1 副标题

1.5.2 渲染规格属性列表

规格属性列表将来会有事件和动态效果。所以需要有js代码参与,不能使用Thymeleaf来渲染了。

因此,这里要用vue,不过需要先把数据放到js对象中,方便vue使用

初始化数据

在页面的head中,定义一个js标签,然后在里面定义变量,保存与sku相关的一些数据:

  • specialParamName:特有规格参数id和name

  • specialParamValue:特有规格参数id和value

具体数据:

通过Vue渲染

把刚才获得的几个变量保存在Vue实例中:

页面中渲染:

<div id="specification" class="summary-wrap clearfix">
    <dl v-for="(v,k) in specialParamName" :key="k">
        <dt>
            <div class="fl title">
                <i>{{v}}</i>
            </div>
        </dt>
        <dd v-for="(str,j) in specialParamValue[k]" :key="j">
            <a href="javascript:;" class="selected">
                {{str}}<span title="点击取消选择">&nbsp;</span>
            </a>
        </dd>
    </dl>
</div>

效果:

数据成功渲染了。不过发现所有的规格都被勾选了。这是因为现在,每一个规格都有样式:selected,应该只选中一个,让它的class样式为selected才对!

那么问题来了,该如何确定用户选择了哪一个?

1.5.3 规格属性筛选

分析

每一个规格项是数组中的一个元素,因此我们只要保存被选择的规格项的索引,就能判断哪个是用户选择的了!

所以需要一个对象来保存用户选择的索引,格式如下:

{
    "4":0,
    "12":0,
    "13":0
}

但问题是,第一次进入页面时,用户并未选择任何参数。因此索引应该有一个默认值,并且将默认值设置为0。

head的script标签中,对索引对象进行初始化:

然后在vue中保存:

页面改造

效果:

1.5.4 确定sku

在当初设计sku数据的时候,就已经添加了一个字段:indexes:

这其实就是规格参数的索引组合。

而在页面中,用户点击选择规格后,就会把对应的索引保存起来:

因此,可以根据这个indexes来确定用户要选择的sku

在vue中定义一个计算属性,来计算与索引匹配的sku:

刷新页面,进行选择:

1.5.5 渲染sku列表

既然已经拿到了用户选中的sku,接下来,就可以在页面渲染数据了

图片列表

商品图片是一个字符串,所以需要将其分割成数组,然后页面遍历即可

标题和价格

完整效果

1.6 商品详情

商品详情页如下图所示:

分为上下两个部分:

  • 上部:展示的是规格属性列表(不展示特有属性)

  • 下部:展示的是商品详情

1.6.1 属性列表

遍历specName即可,注意过滤特有属性。

1.6.2 商品详情

商品详情是HTML代码,不能使用 th:text,应该使用th:utext

1.7 规格包装

规格包装分为两部分:

  • 规格参数
  • 包装列表

而且规格参数需要按照组来显示

1.7.1 规格参数

最终效果:

构造groups

groups最终的数据结构:

代码:

    private List<Map<String, Object>> getGroupsSpec(List<Map<String, Object>> allSpecs, Map<Integer, String> specName, Map<Integer, Object> specValue) {
        List<Map<String, Object>> groups = new ArrayList<>();
        int i = 0;
        int j = 0;
        for (Map<String,Object> spec :allSpecs){
            List<Map<String, Object>> params = (List<Map<String, Object>>) spec.get("params");
            List<Map<String,Object>> temp = new ArrayList<>();
            for (Map<String,Object> param :params) {
                for (Map.Entry<Integer, String> entry : specName.entrySet()) {
                    if (entry.getValue().equals(param.get("k").toString())) {
                        String value = specValue.get(entry.getKey()) != null ? specValue.get(entry.getKey()).toString() : "无";
                        Map<String, Object> temp3 = new HashMap<>(16);
                        temp3.put("id", ++j);
                        temp3.put("name", entry.getValue());
                        temp3.put("value", value);
                        temp.add(temp3);
                    }
                }
            }
            Map<String,Object> temp2 = new HashMap<>(16);
            temp2.put("params",temp);
            temp2.put("id",++i);
            temp2.put("name",spec.get("group"));
            groups.add(temp2);
        }
        return groups;
    }

解析:

  • 参数

allSpecs(所有规格参数)

{group=主体, params=[{k=品牌, searchable=false, global=true, v=小米(MI)}, {k=型号, searchable=false, global=true, v=MI6}, {k=上市年份, searchable=false, global=true, numerical=true, unit=年, v=2017.0}]}
{group=基本信息, params=[{k=机身颜色, searchable=false, global=false, options=[陶瓷黑尊享版, 亮蓝色, 亮黑色, 亮白色]}, {k=机身重量(g), searchable=false, global=true, numerical=true, unit=g, v=180}, {k=机身材质工艺, searchable=true, global=true, v=null}]}
{group=操作系统, params=[{k=操作系统, searchable=true, global=true, v=Android}]}
{group=主芯片, params=[{k=CPU品牌, searchable=true, global=true, v=骁龙(Snapdragon)}, {k=CPU型号, searchable=false, global=true, v=骁龙835(MSM8998)}, {k=CPU核数, searchable=true, global=true, v=八核}, {k=CPU频率, searchable=true, global=true, numerical=true, unit=GHz, v=2.45}]}
{group=存储, params=[{k=内存, searchable=true, global=false, numerical=false, unit=GB, options=[6GB]}, {k=机身存储, searchable=true, global=false, numerical=false, unit=GB, options=[128GB]}]}
{group=屏幕, params=[{k=主屏幕尺寸(英寸), searchable=true, global=true, numerical=true, unit=英寸, v=5.15}, {k=分辨率, searchable=false, global=true, v=1920*1080}]}
{group=摄像头, params=[{k=前置摄像头, searchable=true, global=true, numerical=true, unit=万, v=800.0}, {k=后置摄像头, searchable=true, global=true, numerical=true, unit=万, v=1200.0}]}
{group=电池信息, params=[{k=电池容量(mAh), searchable=true, global=true, numerical=true, unit=mAh, v=3350.0}]}

specName(id和name键值对)

{1=品牌, 2=型号, 3=上市年份, 4=机身颜色, 5=机身重量(g), 6=机身材质工艺, 7=操作系统, 8=CPU品牌, 9=CPU型号, 10=CPU核数, 11=CPU频率, 12=内存, 13=机身存储, 14=主屏幕尺寸(英寸), 15=分辨率, 16=前置摄像头, 17=后置摄像头, 18=电池容量(mAh)}

specValue(id和value键值对)

{1=小米(MI), 2=MI6, 3=2017年, 4=[陶瓷黑尊享版, 亮蓝色, 亮黑色, 亮白色], 5=180g, 6=null, 7=Android, 8=骁龙(Snapdragon), 9=骁龙835(MSM8998), 10=八核, 11=2.45GHz, 12=[6GB], 13=[128GB], 14=5.15英寸, 15=1920*1080, 16=800万, 17=1200万, 18=3350mAh}
  • 根据预期的数据结构进行抽取,结果如下:
{name=主体, id=1, params=[{name=品牌, id=1, value=小米(MI)}, {name=型号, id=2, value=MI6}, {name=上市年份, id=3, value=2017年}]}
{name=基本信息, id=2, params=[{name=机身颜色, id=4, value=[陶瓷黑尊享版, 亮蓝色, 亮黑色, 亮白色]}, {name=机身重量(g), id=5, value=180g}, {name=机身材质工艺, id=6, value=无}]}
{name=操作系统, id=3, params=[{name=操作系统, id=7, value=Android}]}
{name=主芯片, id=4, params=[{name=CPU品牌, id=8, value=骁龙(Snapdragon)}, {name=CPU型号, id=9, value=骁龙835(MSM8998)}, {name=CPU核数, id=10, value=八核}, {name=CPU频率, id=11, value=2.45GHz}]}
{name=存储, id=5, params=[{name=内存, id=12, value=[6GB]}, {name=机身存储, id=13, value=[128GB]}]}
{name=屏幕, id=6, params=[{name=主屏幕尺寸(英寸), id=14, value=5.15英寸}, {name=分辨率, id=15, value=1920*1080}]}
{name=摄像头, id=7, params=[{name=前置摄像头, id=16, value=800万}, {name=后置摄像头, id=17, value=1200万}]}
{name=电池信息, id=8, params=[{name=电池容量(mAh), id=18, value=3350mAh}]}

引入groups

Vue中保存:

替换特有属性的值

根据用户所选的sku,展示对应的特有属性,可以通过sku中的ownSpec获取到特有属性的值:

因为sku是动态的,所以编写一个计算属性,来进行特有属性值的替换:

specialParamName中保存的值:

替换:

页面渲染

1.7.2 包装列表

直接通过Thymeleaf进行渲染

1.8 售后服务

直接通过Thymeleaf进行渲染

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐