java电商项目(三)

本文涉及的产品
实时计算 Flink 版,5000CU*H 3个月
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
实时数仓Hologres,5000CU*H 100GB 3个月
简介: 本文介绍了乐购商城的商品数据分析和管理功能。首先解释了SPU(标准产品单位)和SKU(库存量单位)的概念,以及它们在商品管理和销售中的作用。接着详细分析了SPU、SPU详情和SKU三个表的结构及其关系。文章还介绍了商品管理的需求分析、实现思路和后台代码,包括实体类、持久层、业务层和控制层的实现。最后,文章讲解了前端组件的设计和实现,包括列表组件、添加修改组件、商品描述、通用规格、SKU特有规格和SKU列表的处理。通过这些内容,读者可以全面了解乐购商城的商品管理和数据分析系统。

[TOC]

1 商品数据分析

1.1 SPU与SKU

乐购商城是一个全品类的电商网站,因此商品的种类繁多,每一件商品,其属性又有差别。为了更准确描述商品及细分差别,抽象出两个概念:SPU和SKU,了解一下:

  • SPU:Standard Product Unit (标准产品单位) ,一组具有共同属性的商品集

  • SKU:Stock Keeping Unit(库存量单位),SPU商品集因具体特性不同而细分的每个商品

以图为例来看:

1526085541996

  • 本页的 华为Mate10 就是一个商品集(SPU)
  • 因为颜色、内存等不同,而细分出不同的Mate10,如亮黑色128G版。(SKU)

可以看出:

  • SPU是一个抽象的商品集概念,为了方便后台的管理。
  • SKU才是具体要销售的商品,每一个SKU的价格、库存可能会不一样,用户购买的是SKU而不是SPU

1.2 表结构分析

spu_ 表 (SPU表)

字段名称 字段含义 字段类型 字段长度 备注
id_ 主键 BIGINT
title_ 标题 VARCHAR
subtitle 子标题 VARCHAR
brandid 品牌ID INT
cid1_ 一级分类 INT
cid2_ 二级分类 INT
cid3_ 三级分类 INT
saleable_ 是否上架 BOOLEAN
valid_ 是否有效 BOOLEAN 逻辑删除用
createtime 创建时间 DATETIME
last_updatetime 最后修改时间 VARCHAR

spudetail 表(spu详情表)

字段名称 字段含义 字段类型 字段长度 备注
id 主键 BIGINT
description_ 商品描述 VARCHAR
genericspec 通用规格参数 VARCHAR
specialspec sku规格参数 VARCHAR
packinglist 包装列表 VARCHAR
afterservice 售后服务 VARCHAR

genericspec

{"品牌":"华为","型号":"G9青春版(全网通版)","上市年份":2016,"机身重量(g)":143,"机身材质工艺":"其它","操作系统":"Android","CPU品牌":"骁龙(Snapdragon)","CPU型号":"骁龙617(msm8952)","CPU核数":"八核","CPU频率":1.5,"主屏幕尺寸(英寸)":5.2,"分辨率":"1920*1080(FHD)","前置摄像头":800,"后置摄像头":1300,"电池容量(mAh)":3000}

specialspec

{"机身颜色":["白色","金色","玫瑰金"],"内存":["3GB"],"机身存储":["16GB"]}

sku_ 表 (sku表)

字段名称 字段含义 字段类型 字段长度 备注
id_ 主键 BIGINT
spuid 外键 VARCHAR spu编号
title_ 标题 VARCHAR
images_ 图片 VARCHAR
price_ 单价 NUMBER(10,2)
indexes_ sku规格的下标 VARCHAR
ownspec sku规格参数 VARCHAR
enable_ 是否有效 BOOLEAN 逻辑删除用
createtime 创建时间 DATETIME
last_updatetime 最后修改时间 DATETIME
stock_ 库存 VARCHAR

ownspec

{"机身颜色":"金","内存":"4GB","机身存储":"32GB"}

通过上面的数据分析,得出这3个表的关系图如下:

2 商品管理

2.1 需求分析

实现商品的新增与修改功能。

(1)第1个步骤,先选择添加的商品所属分类

(2)第2个步骤,填写SPU基本信息

1559294046675

(3)第3个步骤,填写商品描述

1559294162036

(4)第4个步骤,填写通用规格参数

(5)第5个步骤,填写spu参数,这个参数会通过笛卡尔积算法,生成sku列表

(6)第6个步骤,生成的SKU列表

根据sku属性,通过笛卡尔积算法生成SKU列表

2.2 实现思路

前端传递给后端的数据格式 是一个spu对象和sku列表组成的对象,如下图:

1560601796032

上图JSON数据如下:

{
   
    "id": 2,
    "brandId": 8557,
    "cid1": 74,
    "cid2": 75,
    "cid3": 76,
    "title": "华为 G9 青春版测试 ",
    "subTitle": "骁龙芯片!3GB运行内存!索尼1300万摄像头!<a href='https://sale.jd.com/act/DhKrOjXnFcGL.html' target='_blank'>华为新品全面上线,更多优惠猛戳》》</a>",
    "saleable": true,
    "valid": true,
    "createTime": "2018-04-21",
    "lastUpdateTime": "2018-06-18",
    "spuDetail": {
   
        "id": 2,
        "description": "<p><img src=\"//img20.360buyimg.com/vc/jfs/t5893/141/6838703316/1369626/15c9d88f/596c753aN075ee827.jpg\"></p>",
        "specialSpec": "{\"机身颜色\":[\"白色\",\"金色\",\"玫瑰金\"],\"内存\":[\"3GB\"],\"机身存储\":[\"16GB\"]}",
        "genericSpec": "{\"品牌\":\"华为\",\"型号\":\"G9青春版(全网通版)\",\"上市年份\":2016,\"机身重量(g)\":143,\"机身材质工艺\":\"其它\",\"操作系统\":\"Android\",\"CPU品牌\":\"骁龙(Snapdragon)\",\"CPU型号\":\"骁龙617(msm8952)\",\"CPU核数\":\"八核\",\"CPU频率\":1.5,\"主屏幕尺寸(英寸)\":5.2,\"分辨率\":\"1920*1080(FHD)\",\"前置摄像头\":800,\"后置摄像头\":1300,\"电池容量(mAh)\":3000}",
        "packingList": "手机(电池内置)*1,中式充电器*1,数据线*1,半入耳式线控耳机*1,华为手机凭证*1,快速指南*1,取卡针*1,屏幕保护膜(出厂已贴)*1",
        "afterService": "本产品全国联保,享受三包服务,质保期为:一年质保"
    },
    "skus": [
        {
   
            "id": 27359021806,
            "spuId": 2,
            "title": "华为 G9 青春版 白色 移动联通电信4G手机 双卡双待",
            "images": "http://image.leyou.com/images/9/15/1524297313793.jpg",
            "price": 84900,
            "ownSpec": "{\"机身颜色\":\"白色\",\"内存\":\"3GB\",\"机身存储\":\"16GB\"}",
            "indexes": "0_0_0",
            "enable": true,
            "createTime": "2018-04-21",
            "lastUpdateTime": "2018-04-21",
            "stock": 99999
        },
        {
   
            "id": 27359021807,
            "spuId": 2,
            "title": "华为 G9 青春版 金色 移动联通电信4G手机 双卡双待",
            "images": "http://image.leyou.com/images/9/5/1524297314398.jpg",
            "price": 84900,
            "ownSpec": "{\"机身颜色\":\"金色\",\"内存\":\"3GB\",\"机身存储\":\"16GB\"}",
            "indexes": "1_0_0",
            "enable": true,
            "createTime": "2018-04-21",
            "lastUpdateTime": "2018-04-21",
            "stock": 99999
        },
        {
   
            "id": 27359021808,
            "spuId": 2,
            "title": "华为 G9 青春版 玫瑰金 移动联通电信4G手机 双卡双待",
            "images": "http://image.leyou.com/images/15/15/1524297314800.jpg",
            "price": 84900,
            "ownSpec": "{\"机身颜色\":\"玫瑰金\",\"内存\":\"3GB\",\"机身存储\":\"16GB\"}",
            "indexes": "2_0_0",
            "enable": true,
            "createTime": "2018-04-21",
            "lastUpdateTime": "2018-04-21",
            "stock": 99999
        }
    ],
    "brandName": null,
    "categoryName": null
}

2.3 后台代码

2.3.1 实体类

legou-item/legou-item-instance/src/main/java/com/lxs/legou/item/po/Spu.java

package com.lxs.legou.item.po;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lxs.legou.core.po.BaseEntity;
import lombok.Data;

import java.util.Date;
import java.util.List;

@Data
@TableName("spu_")
public class Spu extends BaseEntity {

    @TableField("brand_id_")
    private Long brandId;
    @TableField("cid1_")
    private Long cid1;// 1级类目
    @TableField("cid2_")
    private Long cid2;// 2级类目
    @TableField("cid3_")
    private Long cid3;// 3级类目
    @TableField("title_")
    private String title;// 标题
    @TableField("sub_title_")
    private String subTitle;// 子标题
    @TableField("saleable_")
    private Boolean saleable;// 是否上架
    @TableField("valid_")
    private Boolean valid;// 是否有效,逻辑删除用
    @TableField("create_time_")
    private Date createTime;// 创建时间
    @TableField("last_update_time_")
    private Date lastUpdateTime;// 最后修改时间
    @TableField(exist = false)
    private SpuDetail spuDetail; //Spu详情对象,描述,规格参数,SKU参数等...
    @TableField(exist = false)
    private List<Sku> skus; //spu对应的sku集合
    @TableField(exist = false)
    private String brandName; //品牌名称,查询是显示
    @TableField(exist = false)
    private String categoryName;//分类名称,查询是显示
}

legou-item/legou-item-instance/src/main/java/com/lxs/legou/item/po/SpuDetail.java

package com.lxs.legou.item.po;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lxs.legou.core.po.BaseEntity;
import lombok.Data;

@Data
@TableName("spu_detail_")
public class SpuDetail  extends BaseEntity {

    /**
     * 实体编号(唯一标识)
     */
    @TableId(value = "id_", type = IdType.INPUT)
    protected Long id;
    @TableField("description_")
    private String description;// 商品描述
    @TableField("special_spec_")
    private String specialSpec;// 商品特殊规格的名称及可选值模板
    @TableField("generic_spec_")
    private String genericSpec;// 商品的全局规格属性
    @TableField("packing_list_")
    private String packingList;// 包装清单
    @TableField("after_service_")
    private String afterService;// 售后服务


}

legou-item/legou-item-instance/src/main/java/com/lxs/legou/item/po/Sku.java

package com.lxs.legou.item.po;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lxs.legou.core.po.BaseEntity;
import lombok.Data;

import java.util.Date;

@Data
@TableName("sku_")
public class Sku extends BaseEntity {

    @TableField("spu_id_")
    private Long spuId;
    @TableField("title_")
    private String title;
    @TableField("images_")
    private String images;
    @TableField("price_")
    private Long price;
    @TableField("own_spec_")
    private String ownSpec;// 商品特殊规格的键值对
    @TableField("indexes_")
    private String indexes;// 商品特殊规格的下标
    @TableField("enable_")
    private Boolean enable;// 是否有效,逻辑删除用
    @TableField("create_time_")
    private Date createTime;// 创建时间
    @TableField("last_update_time_")
    private Date lastUpdateTime;// 最后修改时间
    @TableField("stock_")
    private Integer stock;// 库存


}

2.3.2 持久层

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/dao/SpuDao.java

package com.lxs.legou.item.dao;

import com.lxs.legou.core.dao.ICrudDao;
import com.lxs.legou.item.po.Spu;

/**
 * @Title: Spu DAO类
 */
public interface SpuDao extends ICrudDao<Spu> {

}

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/dao/SpuDetailDao.java

package com.lxs.legou.item.dao;

import com.lxs.legou.core.dao.ICrudDao;
import com.lxs.legou.item.po.SpuDetail;

/**
 * @Title: Sku DAO类
 */
public interface SpuDetailDao extends ICrudDao<SpuDetail> {

}

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/dao/SkuDao.java

package com.lxs.legou.item.dao;

import com.lxs.legou.core.dao.ICrudDao;
import com.lxs.legou.item.po.Sku;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * @Title: Sku DAO类
 */
public interface SkuDao extends ICrudDao<Sku> {

    @Select("select * from sku_ where spu_id_ = #{skuId}")
    public List<Sku> findBySkuId(Integer skuId);


}

映射文件

legou-item/legou-item-service/src/main/resources/mybatis/item/SpuDao.xml:

<?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.lxs.legou.item.dao.SpuDao">

   <select id="selectByPage" resultType="Spu">
      select
         a.*,
         b.name_ as brandName,
         c.title_ as categoryName
      from
         spu_ a
      left join
         brand_ b on a.brand_id_ = b.id_
      left join
         category_ c on a.cid3_ = c.id_
      <where>
         <if test="title != null and title != ''">
            and title_ like '%${title}%'
         </if>
         <if test="cid3 != null">
            and cid3_ = #{cid3}
         </if>
         <if test="brandId != null">
            and brand_id_ = #{brandId}
         </if>
      </where>
   </select>

   <select id="selectById" resultMap="spuMap">
      select
         *
      from
         spu_
      where
         id_ = #{id}
   </select>

   <resultMap id="spuMap" type="spu">
      <id column="id_" property="id"></id>
      <association property="spuDetail" javaType="SpuDetail" select="com.lxs.legou.item.dao.SpuDetailDao.selectById" column="id_">
         <id column="id_" property="id"></id>
      </association>
      <collection property="skus" javaType="java.util.List" ofType="sku" select="com.lxs.legou.item.dao.SkuDao.findBySkuId" column="id_">
         <id column="id_" property="id"></id>
      </collection>
   </resultMap>

</mapper>

2.3.3 业务层

接口

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/service/ISpuService.java

package com.lxs.legou.item.service;

import com.lxs.legou.core.service.ICrudService;
import com.lxs.legou.item.po.Spu;

public interface ISpuService extends ICrudService<Spu> {

    /**
     * 保存spu,包括如下表的数据
     *      spu
     *      spuDetail
     *      skus
     * @param spu
     */
    public void saveSpu(Spu spu);

}

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/service/ISkuService.java

package com.lxs.legou.item.service;

import com.lxs.legou.core.service.ICrudService;
import com.lxs.legou.item.po.Sku;

public interface ISkuService extends ICrudService<Sku> {

}

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/service/ISpuDetailService.java

package com.lxs.legou.item.service;

import com.lxs.legou.core.service.ICrudService;
import com.lxs.legou.item.po.SpuDetail;

public interface ISpuDetailService extends ICrudService<SpuDetail> {

}

实现类

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/service/impl/SpuServiceImpl.java

package com.lxs.legou.item.service.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.lxs.legou.core.service.impl.CrudServiceImpl;
import com.lxs.legou.item.po.Sku;
import com.lxs.legou.item.po.Spu;
import com.lxs.legou.item.service.ISkuService;
import com.lxs.legou.item.service.ISpuDetailService;
import com.lxs.legou.item.service.ISpuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class SpuServiceImpl extends CrudServiceImpl<Spu> implements ISpuService {

    @Autowired
    private ISpuDetailService spuDetailService;

    @Autowired
    private ISkuService skuService;

    @Override
    @Transactional
    public void saveSpu(Spu spu) {
        //保存spu
        this.saveOrUpdate(spu);
        //保存spuDetail
        if (null == spu.getSpuDetail().getId()) {
            spu.getSpuDetail().setId(spu.getId());
            spuDetailService.save(spu.getSpuDetail());
        } else {
            spuDetailService.updateById(spu.getSpuDetail());
        }
        //保存sku
        //删除spu的所有sku
        skuService.remove(Wrappers.<Sku>query().eq("spu_id_", spu.getId()));
        //添加新的sku
        for (Sku sku : spu.getSkus()) {
            sku.setSpuId(spu.getId());
            skuService.save(sku);
        }
    }

}

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/service/impl/SkuServiceImpl.java

package com.lxs.legou.item.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.lxs.legou.core.service.impl.CrudServiceImpl;
import com.lxs.legou.item.po.Sku;
import com.lxs.legou.item.service.ISkuService;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SkuServiceImpl extends CrudServiceImpl<Sku> implements ISkuService {


}

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/service/impl/SpuDetailServiceImpl.java

package com.lxs.legou.item.service.impl;

import com.lxs.legou.core.service.impl.CrudServiceImpl;
import com.lxs.legou.item.po.SpuDetail;
import com.lxs.legou.item.service.ISpuDetailService;
import org.springframework.stereotype.Service;

@Service
public class SpuDetailServiceImpl extends CrudServiceImpl<SpuDetail> implements ISpuDetailService {

}

2.3.4 控制层

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/controller/SpuController.java:

package com.lxs.legou.item.controller;

import com.lxs.legou.core.controller.BaseController;
import com.lxs.legou.core.po.ResponseBean;
import com.lxs.legou.item.po.Spu;
import com.lxs.legou.item.service.ISpuService;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @Title:
 */
@RestController
@RequestMapping(value = "/spu")
public class SpuController extends BaseController<ISpuService, Spu> {

    @ApiOperation(value="保存商品信息", notes="保存商品信息")
    @PostMapping("/save-spu")
    public ResponseBean saveSpu(@RequestBody Spu spu) throws Exception {
        ResponseBean rm = new ResponseBean();
        try {
            service.saveSpu(spu);
        } catch (Exception e) {
            e.printStackTrace();
            rm.setSuccess(false);
            rm.setMsg("保存失败");
        }
        return rm;
    }

    @ApiOperation(value="查询所有", notes="查询所有spu")
    @GetMapping("/list-all")
    public List<Spu> selectAll() {
        return service.list(new Spu());
    }

}

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/controller/SpuDetailController.java

package com.lxs.legou.item.controller;

import com.lxs.legou.core.controller.BaseController;
import com.lxs.legou.item.po.SpuDetail;
import com.lxs.legou.item.service.ISpuDetailService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Title:
 */
@RestController
@RequestMapping(value = "/spu-detail")
public class SpuDetailController extends BaseController<ISpuDetailService, SpuDetail> {

}

legou-item/legou-item-service/src/main/java/com/lxs/legou/item/controller/SkuController.java:

package com.lxs.legou.item.controller;

import com.lxs.legou.core.controller.BaseController;
import com.lxs.legou.item.po.Sku;
import com.lxs.legou.item.service.ISkuService;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @Title:
 */
@RestController
@RequestMapping(value = "/sku")
public class SkuController extends BaseController<ISkuService, Sku> {


}

2.3.5 测试

使用Postman访问http://localhost:9005/item/spu/edit/2

2.4 前端组件

商品管理的前端组件比较复杂,还是遵循我们的开发原则,前端vue代码了解,实现思路语法即可,不需要大家从0道1编写,真正要写时,根据文档和vue相关帮助能写出来即可,有兴趣的同学可以自行根据文档编写响应的组件,这里我们只讲解前端实现思路,以帮助我们更好的跟后台对接

2.4.1 列表组件

src/view/item/spu/list.vue:

完整代码:

<template>
    <div>
        <Row>
            <Form ref="formData" :model="formData" :label-width="80">
                <Row style="margin-top: 10px;">
                    <Col span="6">
                        <FormItem label="标题" prop="name">
                            <Input v-model="formData.title" placeholder="标题"></Input>
                        </FormItem>
                    </Col>
                    <Col span="6">
                        <FormItem label="分类" prop="cid3">
                            <select-category2 v-model="formData.cid3"></select-category2>
                        </FormItem>
                    </Col>
                    <Col span="6">
                        <FormItem label="品牌" prop="brandId">
                            <select-brand v-model="formData.brandId"></select-brand>
                        </FormItem>
                    </Col>
                    <Col span="6">
                        <Divider type="vertical"/>
                        <Button type="primary" @click="add">添加</Button>
                        <Button type="primary" @click="removeBatch" style="margin-left: 8px">删除</Button>
                        <Button type="primary" @click="query" style="margin-left: 8px">查询</Button>
                    </Col>
                </Row>
            </Form>
        </Row>

        <div>
            <Table stripe ref="selection" :columns="columns" :data="rows"></Table>
        </div>
        <div class="paging">
            <Page :total="total" :page-size="pageSize" show-sizer show-elevator show-total
                  @on-change="changePage" @on-page-size-change="changePageSize"></Page>
        </div>
    </div>
</template>
<style scoped>
    .paging {
        float: right;
        margin-top: 10px;
    }
</style>
<script>
    import {baseList} from '@/libs/crud/base-list'
    import selectCategory2 from '_c/select/selectCategory2.vue'
    import selectBrand from '_c/select/selectBrand.vue'


    export default {
        mixins: [baseList],
        components: {selectCategory2, selectBrand},
        data() {
            return {
                formData: {
                    title: '',
                    cid3: null,
                    brandId: null
                },
                columns: [
                    {
                        type: 'selection',
                        width: 60,
                        align: 'center'
                    },
                    {
                        title: '商品标题',
                        key: 'title'
                    },
                    {
                        title: '分类',
                        key: 'categoryName'
                    },
                    {
                        title: '品牌',
                        key: 'brandName'
                    },
                    {
                        title: '操作',
                        key: 'action',
                        width: 150,
                        align: 'center',
                        render: (h, params) => {
                            return h('div', [
                                h('Button', {
                                    props: {
                                        type: 'primary',
                                        size: 'small'
                                    },
                                    style: {
                                        marginRight: '5px'
                                    },
                                    on: {
                                        click: () => {
                                            this.edit(params.row.id)
                                        }
                                    }
                                }, '修改'),
                                h('Button', {
                                    props: {
                                        type: 'primary',
                                        size: 'small'
                                    },
                                    on: {
                                        click: () => {
                                            this.remove(params.row.id, params.index)
                                        }
                                    }
                                }, '删除')
                            ])
                        }
                    }
                ]
            }
        }
    }
</script>

2.4.2 添加修改组件

src/view/item/spu/edit.vue

<template>
    <div>
        <Steps :current="step">
            <Step title="基本信息"></Step>
            <Step title="商品描述"></Step>
            <Step title="规格参数"></Step>
            <Step title="SKU属性"></Step>
            <Step title="SKU列表"></Step>
        </Steps>
        <Divider/>
        <!--商品基本信息-->
        <Form ref="form" :model="spu" :rules="ruleValidate" :label-width="80" v-show="step == 0">
            <input type="hidden" v-model="spu.id"/>
            <Row>
                <Col span="12">
                    <FormItem label="分类" prop="cid3">
                        <select-category2 v-model="spu.cid3"></select-category2>
                    </FormItem>
                </Col>
                <Col span="12">
                    <FormItem label="品牌" prop="brandId">
                        <select-brand v-model="spu.brandId"></select-brand>
                    </FormItem>
                </Col>
            </Row>
            <FormItem label="商品标题" prop="title">
                <Input v-model="spu.title"></Input>
            </FormItem>
            <FormItem label="商品买点" prop="subTitle">
                <Input type="textarea" :rows="3" v-model="spu.subTitle"></Input>
            </FormItem>
            <FormItem label="包装清单" prop="packingList">
                <Input type="textarea" :rows="3" v-model="spu.spuDetail.packingList"></Input>
            </FormItem>
            <FormItem label="售后服务" prop="afterService">
                <Input type="textarea" :rows="3" v-model="spu.spuDetail.afterService"></Input>
            </FormItem>
        </Form>
        <!--商品描述-->
        <Editor v-model="spu.spuDetail.description" v-show="step==1"/>
        <!--规格参数-->
        <Form ref="form2" :model="spu" :label-width="200" v-show="step == 2">
            <Row v-for="(value, name, index) in genericSpec" :key="index" style="margin-top: 5px;">
                <FormItem :label="name">
                    <Input v-model="genericSpec[name]"></Input>
                </FormItem>
            </Row>
        </Form>
        <!--sku属性-->
        <div v-show="step == 3">
            <Card v-for="(svalue, skey, sindex) in specialSpec" :key="sindex">
                <p slot="title">
                    {
  {skey}}
                </p>
                <Input v-for="(vvalue, vindex) in svalue" :key="vindex" v-model="specialSpec[skey][vindex]" style="margin-top: 5px; margin-bottom: 5px">
                    <Icon type="md-remove" slot="suffix" @click="specialSpec[skey].splice(vindex, 1)"/>
                </Input>
                <Button @click="addSpecialSpec(skey)">添加</Button>
            </Card>
        </div>
        <!--sku列表-->
        <div v-show="step == 4">
            <Row style="border-bottom: solid #c3c3c3 1px; height: 40px; line-height: 40px;">
                <Col v-for="(value, key, index) in specialSpec" :key="index" span="4">{
  {key}}</Col>
                <Col span="6">价格</Col>
                <Col span="6">库存</Col>
            </Row>
            <Row style="border-bottom: solid #c3c3c3 1px; height: 80px; line-height: 80px" v-for="(sku, index) in spu.skus" :key="index">
                <Col v-for="(value, key, index) in JSON.parse(sku.ownSpec)" :key="index" span="4">{
  {value}}</Col>
                <Col span="6">
                    <Input v-model="sku.price" style="width:200px"></Input>
                </Col>
                <Col span="6">
                    <Input v-model="sku.stock" style="width:200px"></Input>
                </Col>
            </Row>
            <Button type="primary" @click="saveSpu" style="margin-top: 10px;float: right">保存商品信息</Button>
        </div>

        <Divider/>
        <Button type="primary" @click="prev">上一步</Button>
        <Divider type="vertical"/>
        <Button type="primary" @click="next">下一步</Button>
    </div>
</template>

<script>
    import instance from '@/libs/api/index'
    import Qs from 'qs'
    import Editor from '_c/form/Editor.vue'
    import selectCategory2 from '_c/select/selectCategory2.vue'
    import selectBrand from '_c/select/selectBrand.vue'
    import {calcDescartes} from '@/libs/util'

    export default {
        components: {Editor, selectCategory2, selectBrand},
        data() {
            return {
                step: 0,
                spu: {
                    id: null,
                    title: '',
                    subTitle: '',
                    cid3: null,
                    brandId: null,
                    spuDetail: {
                        packingList: '',
                        afterService: '',
                        description: '',
                        specialSpec: '{}',
                        genericSpec: '{}'
                    },
                    skus: []
                }, //spu,格式{title:'',brandId:'', subDetail: {description:'', specialSpec:''...}, skus:[...] ...}
                genericSpec: {}, //规格参数,格式:{"品牌": "华为"...}   (这个变量,因为spu中的genericSpec为字符串)
                specialSpec: {}, //sku属性,格式: {"机身颜色": ["白色","黑色", "金色"], "内存":["16G","326"],机身存储:[]}
                ruleValidate: {
                    cid3: [
                        {required: true, message: '类别不能为空', trigger: 'change', type: 'number'},
                    ],
                    brandId: [
                        {required: true, message: '商品不能为空', trigger: 'change', type: 'number'},
                    ],
                    title: [
                        {required: true, message: '商品标题不能为空', trigger: 'blur'},
                    ]
                }
            }
        },
        methods: {
            //保存规格参数
            saveSpu() {
                instance.post(`/item/spu/save-spu`, this.spu, {
                    headers: {
                        'Content-Type': 'application/json;charset=UTF-8'
                    }
                }).then(response => {
                    this.$Message.success(response.data.msg)
                    this.$router.push({name: `list_item_spu`})
                }).catch(error => {
                    console.log(error)
                })
            },
            //添加sku属性
            addSpecialSpec(key) {
                if (!this.specialSpec[key]) {
                    this.specialSpec[key] = new Array()
                }
                this.specialSpec[key].push('')
            },
            next() {
                if (this.step == 4) {
                    this.step = 0;
                } else {
                    this.step += 1;
                }
            },
            prev() {
                if (this.step == 0) {
                    this.step = 4;
                } else {
                    this.step -= 1;
                }
            },
            // 根据ID加载数据
            get(id) {
                instance.get(`/item/spu/edit/${id}`).then(response => {
                    this.spu = Object.assign(response.data);
                    this.genericSpec = JSON.parse(this.spu.spuDetail.genericSpec); //规格参数,计算属性不能双向绑定
                    this.specialSpec = JSON.parse(this.spu.spuDetail.specialSpec); //sku属性
                }).catch(error => {
                    console.log(error)
                })
            },
            //根据商品分类id查询规格参数
            getSpec(cid) {
                instance.post(`/item/param/list`, Qs.stringify({"cid": this.spu.cid3})).then(response => {
                    /**************************规格参数****************************/
                    let generic = Object.create(null);
                    //规格参数的key
                    let genericKeys = response.data.filter((item) => item.generic === true) .map((item) => item.name)
                    //修改时,取得第一次查询的规格参数,get后的spu中取出响应的属性
                    genericKeys.forEach(item => {
                        generic[item] = this.genericSpec[item]
                    });
                    this.genericSpec = generic
                    /**************************sku属性****************************/
                    let sku = Object.create(null);
                    //sku属性的key
                    let skuKeys = response.data.filter((item) => item.generic === false) .map((item) => item.name)
                    //修改时,取得第一次查询的规格参数,从get后的spu中取出响应的属性
                    skuKeys.forEach(item => {
                        sku[item] = this.specialSpec[item]
                    });
                    this.specialSpec = sku;

                }).catch(error => {
                    console.log(error)
                })
            }
        },
        created() {
            let id = this.$route.query.id;
            if (id) {
                this.get(id)
            }
        },
        computed: {
            getCid() {
                return this.spu.cid3;
            }
        },
        watch: {
            //规格参数改变,改变spu.spDetail.genericSpec
            genericSpec: {
                handler(newValue, oldValue) {
                    this.spu.spuDetail.genericSpec = JSON.stringify(newValue)
                },
                immediate: false, //第一次回调时取消侦听
                deep: true //发现对象内部值的变化
            },
            //sku属性改变时,改变spu.spuDetail.genericSpec和spu.skus
            specialSpec: {
                handler(newValue, oldValue) {
                    //修改时,在没有更改sku属性时,不进行sku计算
                    if (this.spu.spuDetail.specialSpec == JSON.stringify(newValue)) {
                        return
                    }
                    this.spu.spuDetail.specialSpec = JSON.stringify(newValue)
                    //求sku的笛卡尔积
                    let values = Object.values(this.specialSpec) //[["白色", 黑色, 玫瑰金], [3G, 8G], undefined],没有添加完整是有可能是undefined
                    let keys = Object.keys(this.specialSpec)
                    values = values.filter((value) => value); //去掉undefined
                    let descartes = calcDescartes(values)
                    let skus = new Array()
                    descartes.forEach((ditem, dindex) => {
                        let sku = {}
                        let ownSpec = {} //sku的当前sku属性
                        if (ditem instanceof Array) { //笛卡尔积[["白色", 3G, 16G], [黑色, 3G, 16G]]
                            ditem.forEach((vitem, vindex) => {
                                ownSpec[keys[vindex]] = vitem
                            });
                        } else { //笛卡尔积["V9", "V10", "V20", ...]
                            ownSpec[keys[0]] = ditem
                        }
                        sku.ownSpec = JSON.stringify(ownSpec)
                        sku.title = this.spu.title
                        sku.spuId = this.spu.id
                        skus.push(sku)
                    })
                    this.spu.skus = skus
                },
                immediate: false,
                deep: true
            },
            //选择分类,改变规格参数
            getCid(newValue, oldValue) {
                this.getSpec(newValue);
            }
        }
    }
</script>

代码逻辑比较复杂分开讲解下:

2.4.2.1 基本布局

组件分为一个Steps,有5个Step,每一个Step对应一个组件(基本信息组件,商品描述,规格参数,SKU属性,SKU列表)

2.4.2.2 商品描述
        <!--商品描述-->
        <Editor v-model="spu.spuDetail.description" v-show="step==1"/>

商品描述采用quill-editor富文本编辑器,这里进行了自定义扩展,扩展富文本编辑器的图片上传等功能

src/components/form/Editor.vue:

<template>
    <div>
        <!-- iview图片上传-->
        <Upload
                class="avatar-uploader"
                name="file"
                :action="serverUrl"
                :headers="header"
                :on-success="uploadSuccess"
                :on-error="uploadError"
                :on-format-error="handleFormatError"
                :before-upload="beforeUpload"
                multiple
                :show-upload-list="false"
                :format="['jpg','jpeg','png']"
                :max-size="maxSize"
        >
        </Upload>
        <quill-editor
                class="editor"
                v-model="content"
                ref="myQuillEditor"
                :options="editorOption"
                @blur="onEditorBlur($event)" @focus="onEditorFocus($event)"
                @change="onEditorChange($event)">
        </quill-editor>
    </div>
</template>
<script>
    // 工具栏配置
    const toolbarOptions = [
        ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
        ["blockquote", "code-block"], // 引用  代码块
        [{ header: 1 }, { header: 2 }], // 1、2 级标题
        [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
        [{ script: "sub" }, { script: "super" }], // 上标/下标
        [{ indent: "-1" }, { indent: "+1" }], // 缩进
        // [{'direction': 'rtl'}],                         // 文本方向
        [{ size: ["small", false, "large", "huge"] }], // 字体大小
        [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
        [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
        [{ font: [] }], // 字体种类
        [{ align: [] }], // 对齐方式
        ["clean"], // 清除文本格式
        ["link", "image", "video"] // 链接、图片、视频
    ];

    import { quillEditor } from "vue-quill-editor";
    import "quill/dist/quill.core.css";
    import "quill/dist/quill.snow.css";
    import "quill/dist/quill.bubble.css";

    export default {
        props: {
            /*编辑器的内容*/
            value: {
                type: String
            },
            /*图片大小*/
            maxSize: {
                type: Number,
                default: 4000 //kb
            }
        },

        components: {
            quillEditor
        },
        data() {
            return {
                content: this.value,
                quillUpdateImg: false, // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
                editorOption: {
                    placeholder: "请输入内容",//输入框提示文字
                    theme: "snow", // or 'bubble'
                    modules: {
                        toolbar: {
                            container: toolbarOptions,
                            // container: "#toolbar",
                            handlers: {
                                image: function(value) {
                                    if (value) {
                                        // 触发input框选择图片文件
                                        document.querySelector(".avatar-uploader input").click();
                                    } else {
                                        this.quill.format("image", false);
                                    }
                                },
                                // link: function(value) {
                                //   if (value) {
                                //     var href = prompt('请输入url');
                                //     this.quill.format("link", href);
                                //   } else {
                                //     this.quill.format("link", false);
                                //   }
                                // },
                            }
                        }
                    }
                },
                serverUrl: 'http://localhost:9004/uploadFile', // 这里写你要上传的图片服务器地址
                header: {      //请求头,请按个人接口设置
                    //Accept: 'application/json',
                    //token: `token`,
                }
            };
        },
        watch: {
            // Watch content change
            value(newVal) {
                this.content = newVal
            },
        },
        methods: {
            onEditorBlur() {
                //失去焦点事件
            },
            onEditorFocus() {
                //获得焦点事件
            },
            onEditorChange() {
                //内容改变事件
                this.$emit("input", this.content);
            },

            // 富文本图片上传前
            beforeUpload() {
                // 显示loading动画
                this.quillUpdateImg = true;
            },

            uploadSuccess(res, file) {
                // res为图片服务器返回的数据
                // 获取富文本组件实例
                let quill = this.$refs.myQuillEditor.quill;
                // 如果上传成功
                // if (res.errorCode === 0) {
                //     // 获取光标所在位置
                //     let length = quill.getSelection().index;
                //     // 插入图片  res.url为服务器返回的图片地址
                //     console.log(res.result.url)
                //     quill.insertEmbed(length, "image", res.result.url);
                //     // 调整光标到最后
                //     quill.setSelection(length + 1);
                // } else {
                //     this.$Message.error("图片插入失败");
                // }
                if (res) {
                    // 获取光标所在位置
                    let length = quill.getSelection().index;
                    // 插入图片  res.url为服务器返回的图片地址
                    quill.insertEmbed(length, "image", `http://192.168.220.110:8080/${res}`);
                    // 调整光标到最后
                    quill.setSelection(length + 1);
                } else {
                    this.$Message.error("图片插入失败");
                }
                // loading动画消失
                this.quillUpdateImg = false;
            },
            // 富文本图片上传失败
            uploadError() {
                // loading动画消失
                this.quillUpdateImg = false;
                this.$Message.error("图片插入失败");
            },
            handleFormatError() {
                this.$Message.error('上传文件格式不正确');
            },
            handleMaxSize() {
                this.$Message.error('上传文件过大');
            },
        }
    };
</script>

<style>
    .avatar-uploader {
        display: none;
    }
    .editor {
        line-height: normal !important;
    }
    .editor .ql-container {
        height: 350px;        /*设置输入框高度*/
    }
    .ql-editor.ql-blank::before {
        /* content: '请输入内容' !important; */
        font-style: normal;
        color: rgba(0,0,0,0.3);
    }
    .ql-snow .ql-tooltip[data-mode=link]::before {
        content: "请输入链接地址:";
    }
    .ql-snow .ql-tooltip.ql-editing a.ql-action::after {
        border-right: 0px;
        content: '保存';
        padding-right: 0px;
    }

    .ql-snow .ql-tooltip[data-mode=video]::before {
        content: "请输入视频地址:";
    }

    .ql-snow .ql-picker.ql-size .ql-picker-label::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item::before {
        content: '14px';
    }
    .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
        content: '10px';
    }
    .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
        content: '18px';
    }
    .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
        content: '32px';
    }

    .ql-snow .ql-picker.ql-header .ql-picker-label::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item::before {
        content: '文本';
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
        content: '标题1';
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
        content: '标题2';
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
        content: '标题3';
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
        content: '标题4';
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
        content: '标题5';
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
        content: '标题6';
    }

    .ql-snow .ql-picker.ql-font .ql-picker-label::before,
    .ql-snow .ql-picker.ql-font .ql-picker-item::before {
        content: '标准字体';
    }
    .ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,
    .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
        content: '衬线字体';
    }
    .ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,
    .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
        content: '等宽字体';
    }
</style>

图片上传服务器

serverUrl: 'http://localhost:9004/uploadFile', // 这里写你要上传的图片服务器地址

图片回显

quill.insertEmbed(length, "image", `http://192.168.220.110:8080/${res}`);
2.4.3 通用规格

列表

数据

2.4.4 SKU特有规格

SKU特有规格列表

sku特有规格数据

2.4.5 SKU列表

sku列表数据

2.4.6 加载数据
        created() {
            let id = this.$route.query.id;
            if (id) { //id不等于null加载数据
                this.get(id)
            }
        },

        // 根据ID加载数据
        get(id) {
            instance.get(`/item/spu/edit/${id}`).then(response => {
                this.spu = Object.assign(response.data);
                this.genericSpec = JSON.parse(this.spu.spuDetail.genericSpec); //规格参数计算属性不能双向绑定
                  this.specialSpec = JSON.parse(this.spu.spuDetail.specialSpec); //sku属性
            }).catch(error => {
                console.log(error)
            })
        },
2.4.7 规格参数处理

逻辑

三级分类数据显示

spu.cid3的计算属性

计算属性

计算属性的监听器

监听器

根据cid3的变化查询对应的规格的方法

2.4.8 计算sku列表

这里我们在specialSpec监听其中,监听其变化,如果spu特有规格变化,使用笛卡尔积算法得到相应的sku列表,举例如下

如果sku特有规格

specialSpec = {"机身颜色:["白色", "黑色"], 内存:["3G","16G"]}
specialSpec.values= [["白色", "黑色"], ["3G","16G"]]

得到的sku列表结果为4个sku如下:

[
    ["白色", "3G"],
    ["白色", "16G"],    
    ["黑色", "3G"],    
    ["黑色", "16G"]
]

具体代码如下

            //sku属性改变时,改变spu.spuDetail.genericSpec和spu.skus
            specialSpec: {
                handler(newValue, oldValue) {
                    //修改时,在没有更改sku属性时,不进行sku计算
                    if (this.spu.spuDetail.specialSpec == JSON.stringify(newValue)) {
                        return
                    }
                    this.spu.spuDetail.specialSpec = JSON.stringify(newValue)
                    //求sku的笛卡尔积
                    let values = Object.values(this.specialSpec) //[["白色", 黑色], [3G, 8G], undefined],没有添加完整是有可能是undefined
                    let keys = Object.keys(this.specialSpec)
                    values = values.filter((value) => value); //去掉undefined
                    let descartes = calcDescartes(values) //笛卡尔积[["白色", 3G], [黑色, 3G],["白色",8G],["黑色", 8G]]
                    let skus = new Array()
                    descartes.forEach((ditem, dindex) => {
                        let sku = {}
                        let ownSpec = {} //sku的当前sku属性
                        if (ditem instanceof Array) {
                            ditem.forEach((vitem, vindex) => {
                                ownSpec[keys[vindex]] = vitem
                            });
                        } else { //笛卡尔积["V9", "V10", "V20", ...]
                            ownSpec[keys[0]] = ditem
                        }
                        sku.ownSpec = JSON.stringify(ownSpec)
                        sku.title = this.spu.title
                        sku.spuId = this.spu.id
                        skus.push(sku)
                    })
                    this.spu.skus = skus
                },
                immediate: false,
                deep: true
            },
2.4.9 商品保存

上述数据都处理好后,我们提交spu对象给后端商品微服务,后端商品微服务使用@RequestBody接收spu进行保存

后端代码

前端保存代码

提交的spu数据结构

目录
相关文章
|
2天前
|
安全 NoSQL Java
java电商项目(十)
本文介绍了电商系统中订单结算和下单流程的实现。主要包括: 1. **订单结页**: - **收件地址分析**:用户从购物车页面跳转到订单结算页,加载用户收件地址。地址信息存储在 `address_` 表中。 - **实现用户收件地址查询**:通过用户登录名查询收件地址,涉及实体类、DAO、Service 和 Controller 的实现。 2. **下单**: - **业务分析**:下单时创建订单数据,包括订单表 `order_` 和订单明细表 `order_item_`,同时修改商品库存、增加用户积分并删除购物车数据。
13 3
|
2天前
|
消息中间件 安全 Java
java电商项目(十一)
本文接续前几个文章的项目进行讲解!
8 1
|
2天前
|
缓存 NoSQL Java
java电商项目(十二)
本文接续前几个文章的项目进行讲解
11 1
|
2天前
|
存储 NoSQL Java
java电商项目(九)
本文介绍了购物车功能的实现过程,包括用户登录后将商品添加至购物车、购物车列表展示及微服务之间的认证机制。具体步骤如下: 1. **购物车功能**: - 用户选择商品并点击“加入购物车”,系统将商品信息存储到Redis中。 2. **微服务之间认证**: - **传递管理员令牌**:在授权中心微服务调用用户微服务时,生成管理员令牌并通过Header传递。 - **传递当前用户令牌**:用户登录后,通过Feign拦截器将用户令牌传递到其他微服务。 - **获取用户数据**:通过`SecurityContextHolder`获取用户信息,并使用公钥解密令牌以验证用户
9 1
|
2天前
|
监控 算法 Java
java电商项目(七)
微服务网关作为系统唯一对外的入口,位于客户端和服务端之间,处理非业务功能,如路由请求、鉴权、监控、缓存、限流等。它解决了客户端直接调用多个微服务带来的复杂性、跨域请求、认证复杂、难以重构等问题。常用的微服务网关技术有Nginx、Zuul和Spring Cloud Gateway。Spring Cloud Gateway因其集成断路器、路径重写和较好的性能而被广泛使用。本文介绍了如何使用Spring Cloud Gateway搭建后台网关系统,包括引入依赖、配置文件、跨域配置、路由过滤配置、负载均衡、限流等。此外,还详细讲解了RBAC权限数据管理、组织机构管理单点登录(SSO)及JWT鉴权的实现
8 1
|
2天前
|
canal 监控 JavaScript
java电商项目(六)
Thymeleaf 是一个类似于 FreeMarker 的模板引擎,能够完全替代 JSP。它支持动静结合,无网络时显示静态内容,有网络时用后台数据替换静态内容,并且与 Spring Boot 完美整合。本文介绍了如何使用 Thymeleaf 生成商品详情页的静态页面。具体步骤包括创建商品静态化微服务、配置项目依赖、创建 Controller 和 Service、生成静态页面、模板填充、静态资源过滤以及启动测试。此外,还介绍了如何通过 Canal 监听商品数据变化,自动触发静态页面的生成或删除。
8 1
|
2天前
|
SQL 自然语言处理 Java
java电商项目(五)
本文介绍了如何构建一个基于Elasticsearch的商品搜索微服务,主要包括以下几个部分: 1. **数据导入ES**: - 搭建搜索工程,创建`legou-search`项目,提供搜索服务和索引数据更新操作。 - 配置`pom.xml`文件,引入必要的依赖。 - 创建启动器和配置文件,配置Elasticsearch连接信息。 - 分析索引库数据格式,确定需要存储的数据字段。 - 实现商品微服务接口,调用其他微服务获取所需数据。 - 创建索引并导入数据,将SPU和SKU数据转换为索引库中的Goods对象。 2. **商品搜索*
10 1
|
2天前
|
canal NoSQL 关系型数据库
java电商项目(四)
本章介绍了如何通过Lua、OpenResty、Nginx限流及Canal的使用,实现电商门户首页的高并发解决方案。主要内容包括: 1. **商城门户搭建**:使用Vue和iView构建前端门户项目,介绍如何展示商品分类和广告数据,并通过Redis缓存提升访问速度。 2. **Lua基础**:介绍Lua的基本概念、特性、应用场景及安装步骤,并通过示例展示了Lua的基本语法和常用功能。 3. **OpenResty介绍**:详细说明OpenResty的特性和优势,包括如何安装OpenResty和配置Nginx,以及如何使用Lua脚本操作Nginx缓存和数据库。
8 1
|
2天前
|
存储 前端开发 JavaScript
java电商项目(二)
本文档详细介绍了商品分类和规格参数的实现过程。商品分类分为三级管理,主要用于首页商品导航和后台商品管理,采用树状结构存储。规格参数则用于描述商品的具体属性,包括SPU和SKU的定义,规格参数与分类绑定,支持搜索过滤。文档涵盖了表结构设计、实体类、持久层、业务层、控制层的实现,并提供了前端组件的示例代码,确保前后端无缝对接。
10 1
|
2天前
|
存储 安全 Java
java电商项目(八)
OAuth 2.0 是一种开放标准,允许用户授权第三方应用访问其在某一网站上的私密资源,而无需提供用户名和密码。它通过提供一个令牌(token)来实现这一功能。OAuth 2.0 主要包括四种授权模式:授权码模式、简化模式、密码模式和客户端模式。授权码模式是最常用的一种,适用于第三方平台登录功能。Spring Security OAuth 2.0 提供了强大的工具来实现授权服务器和资源服务器的集成,支持多种授权模式和令牌存储方式,如内存、数据库、JWT 和
10 0