乐优商城(二十二)——商品详情及静态化
目录一、商品详情1.1 商品详情页服务1.1.1 创建module1.1.2 pom依赖1.1.3 编写启动类1.1.4 application.yml1.1.5 页面模板1.2 页面跳转1.2.1 修改页面跳转路径1.2.2 nginx反向代理1.2.3 编写跳转Controller1.2.4 测试1.3 封装模型数据1.3.1 商品微服务...
目录
一、商品详情
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="点击取消选择"> </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进行渲染
更多推荐
所有评论(0)