微服务商城系统 实战记录:生成商品详情页

一、需求分析

     当系统审核完成商品,需要将商品详情页进行展示,采用静态页面生成的方式。开发流程如下图所示:
在这里插入图片描述

执行步骤:

  • 系统管理员(商家运维人员)修改或者审核商品的时候,会触发 canal 监控数据
  • canal 微服务获取修改数据后,调用 静态页微服务的方法 生成静态页
  • 静态页微服务只负责使用 thymeleaf 的模板技术生成静态页

二、商品静态化微服务创建

1、搭建项目

(1)在changgou-web下创建一个名称为 changgou-web-item 的模块 :
在这里插入图片描述

(2)changgou-web-item中导入依赖:

<?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>changgou-web</artifactId>
        <groupId>com.changgou</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>changgou-web-item</artifactId>

    <dependencies>
        <!--api 模块-->
        <dependency>
            <groupId>com.changgou</groupId>
            <artifactId>changgou-service-goods-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

(3)修改application.yml的配置

server:
  port: 18088
Spring:
  resources:
    static-locations: classpath:/
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true
spring:
  thymeleaf:
    cache: false
  application:
    name: item
  main:
    allow-bean-definition-overriding: true
# 生成静态页的位置
pagepath: D:\codes\ChangGou\changgou-parent\changgou-web\changgou-web-item\src\main\resources\static

(4)创建系统启动类

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.changgou.goods.feign")
public class ItemApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(ItemApplication.class,args);
    }
}

(5)生成静态页
     页面发送请求,传递要生成的静态页的的商品的 SpuID,后台 controller 接收请求,调用 thyemleaf 的原生 API 生成商品静态页。
在这里插入图片描述

     可以看到,需要查询 SPU 的 3个分类 作为面包屑显示,同时还需要查询 SKU 和 SPU 信息。

2、Feign 创建

    因为需要查询 SPU 、SKU 以及 Category,所以我们需要先创建Feign,在 changgou-service-goods-api 模块中提供 CategoryFeign 类,提供根据 ID 查询分类数据的方法:

扫描二维码关注公众号,回复: 12904502 查看本文章
@FeignClient(value="goods")
@RequestMapping("/category")
public interface CategoryFeign {
    
    
    @GetMapping("/{id}")
    public Result<Category> findById(@PathVariable(name = "id") Integer id);
}

在 changgou-service-goods-api 中提供 SkuFeign,提供 根据 SpuID 查询 Sku 集合的方法:

@FeignClient(value="goods")
@RequestMapping("/sku")
public interface SkuFeign {
    
    
 @PostMapping(value = "/search" )
    public Result<List<Sku>> findList(@RequestBody(required = false) Sku sku);
  }

在changgou-service-goods-api 中提供 SpuFeign,提供 根据 SpuID 查询 Spu 信息的方法:

@FeignClient(value="goods")
@RequestMapping("/spu")
public interface SpuFeign {
    
    

    @GetMapping("/{id}")
    public Result<Spu> findById(@PathVariable(name = "id") Long id);
}

3、 生成静态页实现

(1)创建 service
提供接口:

public interface PageService {
    
    
    /**
     * 根据商品 ID 生成静态页
     * @param spuId
     */
    public void createPageHtml(Long spuId);
}

实现:

public class PageServiceImpl implements PageService{
    
    

    @Autowired
    SpuFeign spuFeign;

    @Autowired
    CategoryFeign categoryFeign;

    @Autowired
    SkuFeign skuFeign;

    @Autowired
    TemplateEngine templateEngine;

    @Value("${pagePath}")
    private String pagepath;

    private Map<String,Object> buildDataModel(Long spuId){
    
    
        // 构建数据模型
        Map<String,Object> dataMap=new HashMap<>();

        // 根据 ID 查找 spu 信息
        Result<Spu> result= spuFeign.findById(spuId);
        Spu spu=result.getData();

        dataMap.put("spu",spu);

        // 获取分类信息,一、二、三级分类,用于显示面包屑数据
        dataMap.put("category1",categoryFeign.findById(spu.getCategory1Id()).getData());
        dataMap.put("category2",categoryFeign.findById(spu.getCategory2Id()).getData());
        dataMap.put("category3",categoryFeign.findById(spu.getCategory3Id()).getData());

        if(spu.getImage()!=null){
    
    
            dataMap.put("imageList",spu.getImage().split(","));
        }

        dataMap.put("specificationList", JSON.parseObject(spu.getSpecItems(),Map.class));

        // 根据 spuID 查 sku 信息
        Sku skuCondition=new Sku();
        skuCondition.setSpuId(spu.getId());
        Result<List<Sku>> resultSku=skuFeign.findList(skuCondition);
        dataMap.put("skuList",resultSku.getData());

        return dataMap;
    }

    /**
     * 生成静态页
     * @param spuId
     */
    @Override
    public void createPageHtml(Long spuId) {
    
    

        // 上下文
        Context context=new Context();
        Map<String, Object> dataModel=buildDataModel(spuId);
        context.setVariables(dataModel);

        // 准备文件
        File dir=new File(pagepath);
        if(!dir.exists()){
    
    
            dir.mkdir();
        }

        File dest=new File(dir,spuId+".html");

        // 生成页面
        try(PrintWriter writer=new PrintWriter(dest,"UTF-8")) {
    
    
            templateEngine.process("item",context,writer);
        } catch (FileNotFoundException e) {
    
    
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
    
    
            e.printStackTrace();
        }
    }
}

(2)创建 Controller
在 changgou-web-item 中创建 com.changgou.item.controller.PageController 用于接收请求,生成静态页:

@RestController
@RequestMapping("/page")
public class PageController {
    
    

    @Autowired
    private PageService pageService;

    /**
     * 生成静态页面
     * @param id
     * @return
     */
    @RequestMapping("/createHtml/{id}")
    public Result createHtml(@PathVariable(name="id") Long id){
    
    
        pageService.createPageHtml(id);
        return new Result(true, StatusCode.OK,"ok");
    }
}

4、模板填充

(1)面包屑数据

修改 item.html,填充三个分类数据作为面包屑,代码如下:

<div class="crumb-wrap">
    <ul class="sui-breadcrumb">
        <li>
            <a href="#"  th:text="${category1.name}"></a>
        </li>
        <li>
            <a href="#"  th:text="${category2.name}"></a>
        </li>
        <li>
            <a href="#"  th:text="${category3.name}"></a>
        </li>
    </ul>
</div>

(2)商品图片
修改 item.html,将商品图片信息输出,在真实工作中需要做空判断,代码如下:

<div class="fl preview-wrap">
    <!--放大镜效果-->
    <div class="zoom">
        <!--默认第一个预览-->
        <div id="preview" class="spec-preview">
            <span class="jqzoom"><img th:jqimg="${imageList[0]}" th:src="${imageList[0]}" /></span>
        </div>
        <!--下方的缩略图-->
        <div class="spec-scroll">
            <a class="prev">&lt;</a>
            <!--左右按钮-->
            <div class="items">
                <ul>
                    <li th:each="img:${imageList}"><img th:src="${img}"  th:bimg="${img}" onmousemove="preview(this)" /></li>
                </ul>
            </div>
            <a class="next">&gt;</a>
        </div>
    </div>
</div>

(3)规格输出

<div id="specification" class="summary-wrap clearfix">
    <!--循环MAP-->
    <dl th:each="spec,specStat:${specificationList}">
        <dt>
            <div class="fl title">
                <i th:text="${spec.key}"></i>
            </div>
        </dt>
        <dd th:each="arrValue:${specStat.current.value}">
            <a href="javascript:;" >
                <i th:text="${arrValue}"></i>
                <span title="点击取消选择">&nbsp;</span>
            </a>
        </dd>
    </dl>

</div>

(4)默认 SKU 显示
静态页生成后,需要显示默认的 Sku,我们这里默认显示第 1 个 Sku 即可,这里可以结合着Vue 一起实现。可以先定义一个集合,再定义一个 spec 和 sku,用来存储当前选中的 Sku 信息和 Sku 的规格,代码如下:

   <script th:inline="javascript">
        var item = new Vue({
     
     
            el: '#itemArray',
            data: {
     
     
                skuList : [[${
     
     skuList}]],
                sku:{
     
     },
                spec:{
     
     }
            },
            created:function () {
     
     
                this.sku=JSON.parse(JSON.stringify(this.skuList[0]));
                this.spec=JSON.parse(this.skuList[0].spec);
            }

        })
    </script>

     new Vue() 是构建一个新的 Vue 实例,el 表示为实例提供挂载元素,挂载是指用 React 将组件进行渲染,并构造 DOM 元素,然后塞入页面。
    
页面显示 Sku 信息:

<div class="fr itemInfo-wrap" id="itemArray">
    <div class="sku-name">
        <h4 >{
   
   {sku.name}}</h4>
    </div>
    <div class="news"><span th:text="${spu.caption}"></span></div>
    <div class="summary">
        <div class="summary-wrap" >
            <div class="fl title">
                <i>价  格</i>
            </div>
            <div class="fl price">
                <i>¥</i>
                <em>{
   
   {sku.price}}</em>
                <span>降价通知</span>
            </div>
            <div class="fr remark">
                <i>累计评价</i><em>612188</em>
            </div>
        </div>
        <div class="summary-wrap">
            <div class="fl title">
                <i>促  销</i>
            </div>
            <div class="fl fix-width">
                <i class="red-bg">加价购</i>
                <em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em>
            </div>
        </div>
    </div>


    <div class="support">
        <div class="summary-wrap">
            <div class="fl title">
                <i>支  持</i>
            </div>
            <div class="fl fix-width">
                <em class="t-gray">以旧换新,闲置手机回收  4G套餐超值抢  礼品购</em>
            </div>
        </div>
        <div class="summary-wrap">
            <div class="fl title">
                <i>配 送 至</i>
            </div>
            <div class="fl fix-width">
                <em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em>
            </div>
        </div>
    </div>

    <div class="clearfix choose">
        <div id="specification" class="summary-wrap clearfix">
            <!--循环MAP-->
            <dl th:each="spec,specStat:${specificationList}">
                <dt>
                    <div class="fl title">
                        <i th:text="${spec.key}"></i>
                    </div>
                </dt>
                <dd th:each="arrValue:${specStat.current.value}">
                    <a href="javascript:;"  th:v-bind:class="|{selected:sel('${spec.key}','${arrValue}')}|" th:@click="|selectSpecification('${spec.key}','${arrValue}')|"  >
                        <i th:text="${arrValue}"></i>
                        <span title="点击取消选择">&nbsp;</span>
                    </a>
                </dd>
            </dl>

        </div>

        <div class="summary-wrap">
            <div class="fl title">
                <div class="control-group">
                    <div class="controls">
                        <input autocomplete="off" type="text" value="1" minnum="1" class="itxt" />
                        <a href="javascript:void(0)" class="increment plus">+</a>
                        <a href="javascript:void(0)" class="increment mins">-</a>
                    </div>
                </div>
            </div>
            <div class="fl">
                <ul class="btn-choose unstyled">
                    <li>
                        <a href="cart.html" target="_blank" class="sui-btn  btn-danger addshopcar">加入购物车</a>
                    </li>
                </ul>
            </div>
        </div>
    </div>
</div>

(5)记录选中的 Sku
在当前 Spu 的所有 Sku 中,spec 值是唯一的,我们可以根据 spec 来判断用户选中的是哪个Sku,代码如下:

// 选择切换
selectSpecification:function(specName,specValue) {
    
    
// 选中的 spec 信息
this.$set(this.spec,specName,specValue);
// 遍历
for(var i=0;i<this.skuList.length;i++ ){
    
    
  if(this.isQ(JSON.parse(this.skuList[i].spec),this.spec)){
    
    

	//this.sku =this.skuList[i];
	// 深克隆
	this.sku=JSON.parse(JSON.stringify(this.skuList[i]));
	// break ;
	return;
  }
}
// 如果没有找到 sku,提示下架操作
this.sku.id=0;
this.sku.name='提示:该商品已下架';
this.sku.price=0;
},

     其中,JSON.parse(JSON.stringify(this.skuList[i])) 用来实现深克隆,它是先利用 JSON.stringify 将js 对象序列化成 JSON字符串,再使用 JSON.parse 来反序列化 即 还原 js 对象。对象本身存储的是一个地址映射,如果断电,对象将不存在,所以要将对象的内容转换成字符串的形式再保存在磁盘上。不过,这种实现深拷贝的方法有局限性,它只适用于一般数据 (对象、数组) 的拷贝。
    
判断两个 JSON 字符串是否相同:

isQ:function (map1,map2){
    
    
 //判断两个对象属性个数是否相等
  if(  Object.keys(map1).length!=Object.keys(map2).length){
    
    
    return false;
  }
  for(var k in map1){
    
    
   if(map1[k]!=map2[k]){
    
    
    return false;
   }
  }
 return true;
}

(6)样式切换
     点击不同规格后,实现样式选中,我们可以 根据每个规格 判断该规格 是否在当前选中的 Sku 规格中,如果在,则返回 true 添加 selected 样式,否则返回 false 不添加 selected 样式:

sel:function(name,value){
    
    
                    if(this.spec == undefined){
    
    
                        return false;
                    }
                    if(this.spec[name]==value){
    
    
                        return true;
                    }else{
    
    
                        return false;
                    }
                },

页面添加样式绑定,代码如下:

1561860653870

5、静态资源过滤

     生成的静态页我们可以先放到 changgou-web-item 工程中,后面项目实战的时候可以挪出来放到 Nginx 指定发布目录。一会儿我们将生成的静态页放到 resources/templates/items 目录下,

提供一个 EnableMvcConfig 类,开启静态资源过滤:

@ControllerAdvice
@Configuration
public class EnableMvcConfig implements WebMvcConfigurer {
    
    

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry){
    
    
        registry.addResourceHandler("/items/**").addResourceLocations("classpath:/templates/items/");
    }
}

     将静态资源导入 changgou-web-item 中:
在这里插入图片描述

     然后,启动 eurekea 服务端、商品微服务、静态化微服务 changgou-web-item,访问生成静态页的路径 http://localhost:18088/page/createHtml/1148477873158365184。注意要输入一个 真实存在的 spu_id ,才能访问到 sku 。
     还要注意,访问 css 的路径:

    <link rel="stylesheet" type="text/css" href="../static/css/all.css" />
    <link rel="stylesheet" type="text/css" href="../static/css/pages-item.css" />
    <link rel="stylesheet" type="text/css" href="../static/css/pages-zoom.css" />

这样才能有 css 效果。
运行结果:
在这里插入图片描述
静态页生成后,访问地址:
在这里插入图片描述

    

6、Canal 监听生成静态页

     当 商品微服务 审核商品之后,应当发送消息,这里采用了 Canal 监控数据变化,Canal 监听到数据的变化,直接调用 feign 生成静态页即可。数据变化后,调用 feign 实现生成静态页:
(1)Feign 创建
     在 changgou-service-api 中创建 changgou-web-item-api ,该工程中主要创建 changgou-web-item 的对外依赖抽取信息。

创建 com.changgou.item.feign.PageFeign:

@FeignClient(name = "item")
@RequestMapping("/page")
public interface PageFeign {
    
    
    
    @RequestMapping("/createHtml/{id}")
    Result createHtml(@PathVariable(name = "id")long id);
    
}

(2)pom.xml依赖
修改 changgou-service-canal 工程的 pom.xml,引入如下依赖:

<!--静态页API 服务-->
<dependency>
    <groupId>com.changgou</groupId>
    <artifactId>changgou-web-item-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

(3)修改 changgou-service-canal 工程中的启动类
在这里插入图片描述
     监听类中,监听商品数据库的 tb_spu 的数据变化,当数据变化的时候,生成静态页 或者 删除静态页。

在原来的监听类中添加如下代码:

	@Autowired
    private PageFeign pageFeign;

    @ListenPoint(destination = "example",
    schema = "changgou_goods",
    table = {
    
    "tb_spu"},
    eventType = {
    
    CanalEntry.EventType.UPDATE,CanalEntry.EventType.INSERT,
    CanalEntry.EventType.DELETE})

    public void onEventCustomSpu(CanalEntry.EventType eventType,CanalEntry.RowData rowData){
    
    
        // 判断操作类型
        if(eventType== CanalEntry.EventType.DELETE){
    
    
            String spuId="";
            List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();
            for (CanalEntry.Column column : beforeColumnsList) {
    
    
                if (column.getName().equals("id")) {
    
    
                    spuId = column.getValue();//spuid
                    break;
                }
            }
            //todo 删除静态页

        }else{
    
    
            //新增 或者 更新
            List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
            String spuId = "";
            for (CanalEntry.Column column : afterColumnsList) {
    
    
                if (column.getName().equals("id")) {
    
    
                    spuId = column.getValue();
                    break;
                }
            }
            //更新 生成静态页
            pageFeign.createHtml(Long.valueOf(spuId));
        }
    }

猜你喜欢

转载自blog.csdn.net/weixin_41750142/article/details/115041322