java电商项目(一)

本文涉及的产品
实时数仓Hologres,5000CU*H 100GB 3个月
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时计算 Flink 版,5000CU*H 3个月
简介: 文档介绍了乐购商城项目的架构设计与实现过程,涵盖需求分析、系统设计、框架搭建及商品微服务的开发。项目采用B2C电商模式,前后端分离架构,使用Spring Boot、Spring Cloud等技术构建。主要内容包括1. **需求分析与架构设计** - 描述了在Docker中启动容器、后台服务、前台门户和后台管理的具体步骤 - 详细介绍了系统的B2C电商模式及其前后端分离的设计理念2. **技术架构** - 项目采用Spring Boot 2.1.9.RELEASE和Spring Cloud Greenwich.SR3 - 系统架构图展示了各个微服务之间的关系和交互

[TOC]

3. 需求分析与架构设计

3.1 需求分析

在docker中启动容器

启动后台服务

注意启动顺序

  1. Registry
  2. Config
  3. auth-center
  4. legou-security
  5. 后面微服务没有顺序

启动前台门户

# 进入目录legou-portal-ui,执行如下命令
npm run dev

1559111851979

启动后台管理

# 进入legou-manager-ui
npm run dev

1559112046165

3.2 系统设计

乐购商城属于B2C电商模式,运营商将自己的产品发布到网站上,会员注册后,在网站上将商品添加到购物车,并且下单,完成线上支付,用户还可以参与秒杀抢购。

3.2.1 前后端分离

商城项目采用前后端分离方式。

以前的JavaWeb项目大多数都是java程序员又当爹又当妈,又搞前端,又搞后端。随着时代的发展,渐渐的许多大中小公司开始把前后端的界限分的越来越明确,前端工程师只管前端的事情,后端工程师只管后端的事情。正所谓术业有专攻,一个人如果什么都会,那么他毕竟什么都不精。

对于后端java工程师:

把精力放在设计模式,spring+springmvc,linux,mysql事务隔离与锁机制,mongodb,http/tcp,多线程,分布式架构,弹性计算架构,微服务架构,java性能优化,以及相关的项目管理等等。

对于前端工程师:

把精力放在html5,css3,vuejs,webpack,nodejs,Google V8引擎,javascript多线程,模块化,面向切面编程,设计模式,浏览器兼容性,性能优化等等。

1559553886871

我们在本课程中提供与项目课程配套的管理后台的前端代码,但是不深入讲解前端的内容。这样我们会将更多的精力放在后端代码的开发上!

3.2.2 技术架构

1560087134452

3.2.3 系统架构图

1560090475333

4 框架搭建

4.1 环境准备

(1)VMware Workstation Pro安装centos7 镜像

(2)安装docker

(3)拉取mySQL镜像,并创建容器

(4)客户端连接mysql容器,建库建表(建库建表语句在资源文件夹中提供)

虚拟机数据:

  • 虚拟机IP:192.168.220.110

  • 虚拟机账号:root 密码:1

  • 数据库端口:3306
  • 数据库账号:root 密码:root

数据库脚本:资料\数据库脚本

4.2 项目结构说明

1559113404367

结构说明:

gateway

网关模块,根据网站的规模和需要,可以将综合逻辑相关的服务用网关路由组合到一起。在这里还可以做鉴权和限流相关操作。

config

配置中心微服务

registry

注册中心

项目版本说明

项目采用spring boot2.1.9.RELEASE,spring cloud Greenwich.SR3。

品牌管理需求描述

4.3 公共工程搭建

4.3.1 父工程搭建

创建父工程 legou-parent ,pom.xml文件中增加配置

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lxs</groupId>
    <artifactId>legou-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>registry</module>
        <module>config</module>
        <module>auth-center</module>
        <module>legou-core</module>
        <module>legou-admin</module>
        <module>gateway</module>
        <module>legou-security</module>
        <module>legou-route</module>
        <module>legou-upload</module>
        <module>legou-item</module>
        <module>legou-search</module>
        <module>legou-common</module>
    </modules>

    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

删除src文件夹

4.3.2 工具类工程

legou-common:工具类工程,拷贝即可

项目结构如下:

1564024579642

4.3.3 基类工程

从提供的素材中拷贝导入

legou-core工程中存放dao,service,controller的基类,其他具体dao,service,controller都是继承自此工程的类

pom.xml

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

    <artifactId>legou-core</artifactId>

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

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

        <!--mybatis pulus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.1.11</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <skip>true</skip>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
4.3.3.1 实体类基类

普通实体类基类

package com.lxs.legou.core.po;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import java.io.Serializable;


@Data
@JsonIgnoreProperties(value = {"handler"})
public abstract class BaseEntity implements Serializable {

   /**
    * 实体编号(唯一标识)
    */
   @TableId(value = "id_", type = IdType.AUTO)
   protected Long id;

}

树形结构实体类基类

package com.lxs.legou.core.po;

import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

/**
 * 树状结构实体类父类
 *
 * @author lxs
 * @file BaseTreeEntity.java
 * @Copyright (C) http://www.lxs.com
 * @email lxosng77@163.com
 * @date 2018/7/13
 */
@Data
@JsonIgnoreProperties(value = {"handler"}) //避免懒加载,json转换报错
public class BaseTreeEntity extends BaseEntity {

    @TableField("order_")
    private Integer order; //排序字段
    @TableField("parent_id_")
    private Long parentId; //父节点id
    @TableField("title_")
    private String title; //节点名称
    @TableField("expand_")
    private Boolean expand = false; //是否展开节点

}

HTTP响应返回结果实体类

package com.lxs.legou.core.po;

import java.io.Serializable;
/**
 * @Title: Controller响应实体
 */

public class ResponseBean implements Serializable {

   private static final long serialVersionUID = 1L;

   /**
    * 成功标记
    */
   private boolean success = true;
   /**
    * 提示信息
    */
   private String msg = "操作成功";
   /**
    * 添加,修改的实体类
    */
   private Object model;

   /**
    * http状态码
    */
   private int code = 200;

   /**
    * session id
    */
   private String token;



   public boolean isSuccess() {
      return success;
   }

   public void setSuccess(boolean success) {
      this.success = success;
   }

   public String getMsg() {
      return msg;
   }

   public void setMsg(String msg) {
      this.msg = msg;
   }

   public Object getModel() {
      return model;
   }

   public void setModel(Object model) {
      this.model = model;
   }

   public int getCode() {
      return code;
   }

   public void setCode(int code) {
      this.code = code;
   }

   public String getToken() {
      return token;
   }

   public void setToken(String token) {
      this.token = token;
   }
}
4.3.3.2 dao基类
package com.lxs.legou.core.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lxs.legou.core.po.BaseEntity;

import java.util.List;

public interface ICrudDao<T extends BaseEntity> extends BaseMapper<T> {

    /**
     * 一般要是用动态sql语句查询
     * @param entity
     * @return
     */
    public List<T> selectByPage(T entity);

}
4.3.3.3 service基类

接口

package com.lxs.legou.core.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.github.pagehelper.PageInfo;
import com.lxs.legou.core.po.BaseEntity;

import java.util.List;

public interface ICrudService<T extends BaseEntity> extends IService<T> {

    public PageInfo<T> listPage(T entity, int pageNum, int pageSize);

    public List<T> list(T entity);

}

实现类

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

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.lxs.legou.core.dao.ICrudDao;
import com.lxs.legou.core.po.BaseEntity;
import com.lxs.legou.core.service.ICrudService;

import java.util.List;

public class CrudServiceImpl<T extends BaseEntity> extends ServiceImpl<ICrudDao<T>, T> implements ICrudService<T> {

    @Override
    public PageInfo<T> listPage(T entity, int pageNum, int pageSize) {
        return PageHelper.startPage(pageNum, pageSize).doSelectPageInfo(() -> {
            baseMapper.selectByPage(entity);
        });
    }

    @Override
    public List<T> list(T entity) {
        return getBaseMapper().selectList(Wrappers.emptyWrapper());
    }

}
4.3.3.4 controller基类
package com.lxs.legou.core.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.pagehelper.PageInfo;
import com.lxs.legou.core.po.BaseEntity;
import com.lxs.legou.core.po.ResponseBean;
import com.lxs.legou.core.json.JSON;
import com.lxs.legou.core.service.ICrudService;
import com.lxs.legou.core.utils.GenericUtil;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;


public abstract class BaseController<S extends ICrudService<T>, T extends BaseEntity> {

    @Autowired
    protected S service;

    protected Logger LOG = LoggerFactory.getLogger(this.getClass());

    /**
     * 域对象类型
     */
    protected Class<T> entityClass;

    public BaseController() {
        this.entityClass = GenericUtil.getSuperGenericClass2(this.getClass());
    }

    /**
     * 加载
     *
     * @param id
     * @return
     * @throws Exception
     */
    @ApiOperation(value="加载", notes="根据ID加载")
    @GetMapping("/edit/{id}")
    public T edit(@PathVariable Long id) throws Exception {
        T entity = service.getById(id);
        afterEdit(entity);
        return entity;
    }

    /**
     * 分页查询
     * @param entity
     * @param page
     * @param rows
     * @return
     */
    @ApiOperation(value="分页查询", notes="分页查询")
    @PostMapping("/list-page")
    @JSON(type = BaseEntity.class ,filter = "desc") //无效
    public PageInfo<T> listPage(T entity,
                          @RequestParam(name = "page", defaultValue = "1", required = false) int page,
                          @RequestParam(name = "rows", defaultValue = "10", required = false) int rows) {
        PageInfo<T> result = service.listPage(entity, page, rows);
        return result;
    }

    /**
     * 根据实体条件查询
     * @return
     */
    @ApiOperation(value="查询", notes="根据实体条件查询")
    @RequestMapping(value = "/list", method = {RequestMethod.POST, RequestMethod.GET})
    @JSON(type = BaseEntity.class ,filter = "desc")
    public List<T> list(T entity) {
        List<T> list = service.list(entity);
        return list;
    }

    /**
     * 增加,修改
     */
    @ApiOperation(value="保存", notes="ID存在修改,不存在添加")
    @PostMapping("/save")
    public ResponseBean save(T entity) throws Exception {
        ResponseBean rm = new ResponseBean();
        try {
            beforeSave(entity); //保存前处理实体类
            service.saveOrUpdate(entity);
            rm.setModel(entity);
        } catch (Exception e) {
            e.printStackTrace();
            rm.setSuccess(false);
            rm.setMsg("保存失败");
        }
        return rm;
    }

    /**
     * 删除
     */
    @ApiOperation(value="删除", notes="根据ID删除")
    @GetMapping("/delete/{id}")
    public ResponseBean delete(@PathVariable Long id) throws Exception {
        ResponseBean rm = new ResponseBean();
        try {
            service.removeById(id);
            rm.setModel(null);
        } catch (Exception e) {
            e.printStackTrace();
            rm.setSuccess(false);
            rm.setMsg("保存失败");
        }
        return rm;
    }

    /**
     * 批量删除
     */
    @ApiOperation(value="删除", notes="批量删除")
    @RequestMapping(value = "/delete", method = {RequestMethod.POST, RequestMethod.GET})
    public ResponseBean delete(@RequestParam List<Long> ids) {
        ResponseBean rm = new ResponseBean();
        try {
            service.removeByIds(ids);
        } catch (Exception e) {
            e.printStackTrace();
            rm.setMsg("删除失败");
            rm.setSuccess(false);
        }
        return rm;
    }

    /**
     * 保存前执行
     * @param entity
     * @throws Exception
     */
    public void beforeSave(T entity) throws Exception {
    }

    /**
     * 模板方法:在加载后执行
     * @param entity
     */
    public void afterEdit(T entity) {

    }

}
4.3.3.5 工具类

拷贝即可

ApplicationContextProvider:获得spring容器中的对象的工具类

spring 容器产生时产生ApplicationContextProvider,因为实现了ApplicationContextAware调用setApplicationContext后,可以直接使用ApplicationContextProvider获得spring容器中的对象

package com.lxs.legou.core.support;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;


@Component
public class ApplicationContextProvider
        implements ApplicationContextAware {

    /**
     * 上下文对象实例
     */
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * 获取applicationContext
     *
     * @return
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 通过name获取 Bean.
     *
     * @param name
     * @return
     */
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    /**
     * 通过class获取Bean.
     *
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    /**
     * 通过name,以及Clazz返回指定的Bean
     *
     * @param name
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}

DateConverterConfig:日期转换器

package com.lxs.legou.core.support;

import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;


@Component
public class DateConverterConfig implements Converter<String, Date> {

    private static final List<String> formarts = new ArrayList<>(4);
    static{
        formarts.add("yyyy-MM");
        formarts.add("yyyy-MM-dd");
        formarts.add("yyyy-MM-dd hh:mm");
        formarts.add("yyyy-MM-dd hh:mm:ss");
    }

    @Override
    public Date convert(String source) {
        String value = source.trim();
        if ("".equals(value)) {
            return null;
        }
        if(source.matches("^\\d{4}-\\d{1,2}$")){
            return parseDate(source, formarts.get(0));
        }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")){
            return parseDate(source, formarts.get(1));
        }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")){
            return parseDate(source, formarts.get(2));
        }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")){
            return parseDate(source, formarts.get(3));
        }else {
            throw new IllegalArgumentException("Invalid boolean value '" + source + "'");
        }
    }

    /**
     * 格式化日期
     * @param dateStr String 字符型日期
     * @param format String 格式
     * @return Date 日期
     */
    public  Date parseDate(String dateStr, String format) {
        Date date=null;
        try {
            DateFormat dateFormat = new SimpleDateFormat(format);
            date = dateFormat.parse(dateStr);
        } catch (Exception e) {

        }
        return date;
    }

}

GenericUtil:得到父类声明的泛化类型

package com.lxs.legou.core.utils;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;


public class GenericUtil {

   public static <T> Class<T> getSuperGenericClass(Class<?> clz) {
      Class<T> result = null;
      //得到当前对象的父类"泛型类型"(也叫参数化类型)
      //superclass == GenericDao<Dept>成为参数化类型
      //superclass == BaseDao不是参数化类型
      Type superclass = clz.getGenericSuperclass();
      //判断类型是否为参数化类型
      if (superclass instanceof ParameterizedType) {
         //把父类类型转换成参数化类型(泛型类型)
         //只有ParameterizedType才能通过getActualTypeArguments得到参数
         ParameterizedType parameterizedType = (ParameterizedType) superclass;
         //得到参数化类型类型的参数
         //types == GenericDao<Dept>的"<Dept>"参数
         Type[] types = parameterizedType.getActualTypeArguments();
         //返回子类传递的类型
         result = (Class<T>) types[0];
      }
      return result;
   }

   public static <T> Class<T> getSuperGenericClass2(Class<?> clz) {
      Class<T> result = null;
      //得到当前对象的父类"泛型类型"(也叫参数化类型)
      //superclass == GenericDao<Dept>成为参数化类型
      //superclass == BaseDao不是参数化类型
      Type superclass = clz.getGenericSuperclass();
      //判断类型是否为参数化类型
      if (superclass instanceof ParameterizedType) {
         //把父类类型转换成参数化类型(泛型类型)
         //只有ParameterizedType才能通过getActualTypeArguments得到参数
         ParameterizedType parameterizedType = (ParameterizedType) superclass;
         //得到参数化类型类型的参数
         //types == GenericDao<Dept>的"<Dept>"参数
         Type[] types = parameterizedType.getActualTypeArguments();
         //返回子类传递的类型
         result = (Class<T>) types[1];
      }
      return result;
   }


}

JSON工具类(拷贝即可)

支持使用@JSON(type = Post.class ,filter = "desc"),过滤不序列化的字段等操作,因为Jackson注解,要么全部序列化,要么全部不序列化

示例:

@RestController
@RequestMapping("/post")
public class PostController extends BaseController<IPostService, Post> {

    /**
     * 演示使用JSON注解过滤属性
     * @return
     */
    @ApiOperation(value="查询", notes="查询所有")
    @RequestMapping(value = "/list", method = {RequestMethod.POST, RequestMethod.GET})
    @JSON(type = Post.class ,filter = "desc")
    public List<Post> list(Post post) {
        List<Post> list = service.list(post);
        return list;
    }

3.4 Eureka微服务搭建

3.4.1 pom.xml依赖

创建模块registry,pom.xml引入依赖

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

    <artifactId>registry</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

</project>

3.4.2 配置

配置文件bootstrap.yml

spring:
  application:
    name: registry

application.yml

server:
  port: 8761

eureka:
  server:
    enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
    eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
  instance:
    hostname: localhost
    prefer-ip-address: true
  client:
    register-with-eureka: false # 是否注册自己的信息到EurekaServer,默认是true
    fetch-registry: false # 是否拉取其它服务的信息,默认是true
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

3.4.3 启动类配置

package com.lxs.legou.registry;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class RegistryApplication {

    public static void main(String[] args) {
        SpringApplication.run(RegistryApplication.class, args);
    }

}

测试访问http://localhost:8761/,效果如下:

1560412105435

3.5 配置中心微服务

配置中心微服务为其他微服务工程,提供统一的配置文件的管理,这里我们采用两种方式管理配置文件

  • native:本地配置文件
  • git:远程配置文件

3.5.1 pom.xml

创建config微服务工程,pom.xml依赖如下

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

    <artifactId>config</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</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-config-server</artifactId>
        </dependency>
    </dependencies>


</project>

3.5.2 配置文件

bootstrap.yml

spring:
  application:
    name: config

application.yml

server:
  port: 8888

eureka:
  client:
    registry-fetch-interval-seconds: 5 #Server服务的列表只读备份,然后缓存在本地。默认30秒
    service-url:
      defaultZone: http://${eureka.instance.hostname}:8761/eureka/
  instance:
    hostname: localhost
    prefer-ip-address: true
    lease-expiration-duration-in-seconds: 10 # 10秒即过期
    lease-renewal-interval-in-seconds: 5 # 5秒一次心跳

spring:
  profiles:
    active: composite,default # 如果要使用本地配置文件,此处需增加composite。多profile时,谁在前面谁的配置优先级就高
  cloud:
    config:
      server:
        bootstrap: true # 提前加载配置文件,保证后续数据库连接正常启动
        default-profile: default
        default-label: master
        composite: # 此配置为使用本地文件,与git脱离关系
          - type: native
            search-locations: file:e:\work\0_lxs\10_xzk_shopping_v2\code\legou\legou-parent\config-repo
        git: #git配置
          uri: git@github.com:lxsong/legou-parent.git
          username: lxsong
          password: ######
          search-paths: config-repo

3.5.3 启动器

package com.lxs.legou.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigServer
public class ConfigApplication {
   

    public static void main(String[] args) {
   
        SpringApplication.run(ConfigApplication.class, args);
    }

}

3.5.4 微服务配置

/config-repo/application.yml:所有配置文件都从此配置文件继承

eureka:
  client:
    register-with-eureka: true #从Eureka Server服务的列表只读备份,然后缓存在本地
    registry-fetch-interval-seconds: 5 #`每隔30秒`会重新获取并更新数据
    service-url:
      defaultZone: http://${eureka.instance.hostname}:8761/eureka/
  instance:
    hostname: localhost
    prefer-ip-address: true # 当调用getHostname获取实例的hostname时,返回ip而不是host名称
    lease-expiration-duration-in-seconds: 10 # 10秒即过期
    lease-renewal-interval-in-seconds: 5 # 5秒一次心跳

feign:
  hystrix:
    enabled: true #开启熔断

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 60000 #熔断超时时间

ribbon:
  ReadTimeout: 60000 #通信超时时间
  ConnectTimeout: 60000 #连接超时时间

spring:
  cloud:
    config:
      uri: http://localhost:8888
      fail-fast: true # 即在获取不到远程配置时,立即失败,但是用下边的配置进行重试
      retry:
        initial-interval: 2000 #最初重试间隔为 1000 毫秒
        max-interval: 10000 #最长重试间隔为 2000 毫秒
        multiplier: 2 #每次重试失败后,重试间隔所增加的倍数
        max-attempts: 10 #最多重试 6 次
  datasource:
    url: jdbc:mysql://192.168.220.110:3306/legou?characterEncoding=utf8&characterSetResults=utf8&autoReconnect=true&failOverReadOnly=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    hikari:
      idle-timeout: 60000
      maximum-pool-size: 30
      minimum-idle: 10
  jackson:
    default-property-inclusion: always
    date-format: yyyy-MM-dd
    time-zone: GMT+8

比如配置admin-service.yml如下:

server:
  port: 9001

mybatis-plus:
  mapper-locations: classpath*:mybatis/*/*.xml
  type-aliases-package: com.lxs.legou.*.po
  configuration:
    # 下划线驼峰转换
    map-underscore-to-camel-case: true
    lazy-loading-enabled: true
    aggressive-lazy-loading: false

logging:
  #file: demo.log
  pattern:
    console: "%d - %msg%n"
  level:
    org.springframework.web: debug
    com.lxs: debug

security:
  oauth2:
    resource:
      jwt:
        key-uri: http://localhost:9098/oauth/token_key #如果使用JWT,可以获取公钥用于 token 的验签

访问http://localhost:8888/admin-service.yml显示如下:

4 商品微服务-品牌管理

4.1 需求分析

创建商品微服务,实现对品牌表的增删改查功能

4.2 表结构分析

品牌表:brand_

字段名称 字段含义 字段类型 字段长度 备注
id_ 品牌id INT
name_ 品牌名称 VARCHAR
image_ 品牌图片地址 VARCHAR
letter_ 品牌的首字母 CHAR
seq_ 排序 INT

4.3 创建工程

创建legou-item父工程,聚合管理两个子工程legou-item-instance和legou-item-service

  • legou-item-instance:存放fegin调用的接口和实体类
  • legou-item-service:存放实际商品微服务

结构如下

4.3.1 legou-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>legou-parent</artifactId>
        <groupId>com.lxs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>legou-item</artifactId>

    <packaging>pom</packaging>
    <modules>
        <module>legou-item-instance</module>
        <module>legou-item-service</module>
    </modules>


</project>

4.3.2 legou-item-instance

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

    <artifactId>legou-item-instance</artifactId>

    <dependencies>

        <dependency>
            <groupId>com.lxs</groupId>
            <artifactId>legou-core</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.core.Starter</mainClass>
                    <layout>ZIP</layout>
                    <classifier>exec</classifier>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

4.3.3 legou-item-service

4.3.3.1 pom.xml
<?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>legou-item</artifactId>
        <groupId>com.lxs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>legou-item-service</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <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.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

        <dependency>
            <groupId>com.lxs</groupId>
            <artifactId>legou-core</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.lxs</groupId>
            <artifactId>legou-item-interface</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

    </dependencies>

</project>
4.3.3.2 配置文件

resources/bootstrap.yml

spring:
  application:
    name: item-service

/config-repo/item-service.yml

server:
  port: 9005

mybatis-plus:
  mapper-locations: classpath*:mybatis/*/*.xml
  type-aliases-package: com.lxs.legou.*.po
  configuration:
    # 下划线驼峰转换
    map-underscore-to-camel-case: true
    lazy-loading-enabled: true
    aggressive-lazy-loading: false

logging:
  #file: demo.log
  pattern:
    console: "%d - %msg%n"
  level:
    org.springframework.web: debug
    com.lxs: debug
4.3.3.3 启动器
package com.lxs.legou.item;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class ItemApplication {

    public static void main(String[] args) {
        SpringApplication.run(ItemApplication.class, args);
    }

}
4.3.3.4 mybatis plus配置类
package com.lxs.legou.item.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
import com.github.pagehelper.PageInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
@MapperScan("com.lxs.legou.item.dao")
public class MybatisPlusConfig {

    /**
     * 分页插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        // 开启 count 的 join 优化,只针对 left join !!!
        return new PaginationInterceptor().setCountSqlParser(new JsqlParserCountOptimize(true));
    }

    @Bean
    public PageInterceptor pageInterceptor() {
        return new PageInterceptor();
    }

}

4.4 品牌管理

4.4.1 实体类

在legou-item-interface工程下创建品牌表对应Brand实体类

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;

/**
 * @file 品牌
 */
@Data
@TableName("brand_")
public class Brand extends BaseEntity {
   

    @TableField("name_")
    private String name;    // 名称
    @TableField("image_")
    private String image;    // 图片
    @TableField("letter_")
    private String letter; //首字母

    @TableField(exist = false)
    private Long[] categoryIds; //瞬时属性,品牌的所属分类如[1,2,3,4]

}

4.4.2 Dao

在legou-item-service微服务下创建legou-item/legou-item-service/src/main/java/com/lxs/legou/item/dao/BrandDao.java接口,代码如下:

package com.lxs.legou.item.dao;

import com.lxs.legou.core.dao.ICrudDao;
import com.lxs.legou.item.po.Brand;
import com.lxs.legou.item.po.Category;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * @Title: 商品Dao
 */
public interface BrandDao extends ICrudDao<Brand> {
   

}

继承了ICrudDao接口,就自动实现了增删改查的常用方法。

因为需要一般查询需要动态SQL语句所以需要些如下映射文件

<?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.BrandDao">

   <select id="selectByPage" resultType="Brand">
      select
         *
      from
         brand_
      <where>
      <if test="name != null and name != ''">
         and name_ like '%${name}%'
      </if>
      </where>
   </select>

</mapper>

注意:selectByPage方法在ICrudDao中定义了,这里只要映射就可以了

4.4.3 Service

接口

package com.lxs.legou.item.service;

import com.lxs.legou.core.service.ICrudService;
import com.lxs.legou.item.po.Brand;
import com.lxs.legou.item.po.Category;

import java.util.List;

/**
 * @Title: 商品业务对象
 */
public interface IBrandService extends ICrudService<Brand> {
   

}

实现类

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.dao.BrandDao;
import com.lxs.legou.item.po.Brand;
import com.lxs.legou.item.po.Category;
import com.lxs.legou.item.service.IBrandService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class BrandServiceImpl extends CrudServiceImpl<Brand> implements IBrandService {
   

}

4.4.4 Controller

package com.lxs.legou.item.controller;

import com.lxs.legou.core.controller.BaseController;
import com.lxs.legou.item.po.Brand;
import com.lxs.legou.item.po.Category;
import com.lxs.legou.item.service.IBrandService;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @Title:
 */
@RestController
@RequestMapping(value = "/brand")
public class BrandController extends BaseController<IBrandService, Brand> {
   

}

4.5.6 测试

访问http://localhost:9005/brank/list-page查看结果如下:

4.5 品牌所属分类

因为添加品牌需要选择品牌的所属分类,而分类是树状结构实体类

需求

4.5.1 表结构

业务分析:

  • 加载品牌时通过categoryIds得到品牌的分类
  • 保存时,先删除品牌已有的分类,然后按照提交的参数添加新的分类

4.5.2 实体类

分类实体类是一个树状结构,所以这里继承BaseTreeEntity,关于分类的管理后面讲解

package com.lxs.legou.item.po;

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

/**
 * @Title: 商品分类
 */
@Data
@TableName("category_")
public class Category extends BaseTreeEntity {

    @TableField("is_parent_")
    private Boolean isParent = false; //是否为父节点

    @TableField(exist = false)
    private Integer isRoot = 0; //值=1 : 查询根节点条件

    public String getLabel() { //treeselect需要的属性
        return this.getTitle();
    }

}

品牌实体类中记录分类属性

4.5.3 持久层

Dao

package com.lxs.legou.item.dao;

import com.lxs.legou.core.dao.ICrudDao;
import com.lxs.legou.item.po.Brand;
import com.lxs.legou.item.po.Category;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * @Title: 商品Dao
 */
public interface BrandDao extends ICrudDao<Brand> {

   /**
    * 删除商品和分类关联
    * @param id
    * @return
    */
   public int deleteCategoryByBrand(Long id);

   /**
    * 关联商品和分类
    * @param categoryId
    * @param brandId
    * @return
    */
   public int insertCategoryAndBrand(@Param("categoryId") Long categoryId, @Param("brandId") Long brandId);

   /**
    * 查询商品的分类
    * @param id
    * @return
    */
   public List<Category> selectCategoryByBrand(Long id);

}

映射文件

<?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.BrandDao">

   <select id="selectByPage" resultType="Brand">
      select
         *
      from
         brand_
      <where>
      <if test="name != null and name != ''">
         and name_ like '%${name}%'
      </if>
      </where>
   </select>

   <delete id="deleteCategoryByBrand">
      delete from
         category_brand_
      where
         brand_id_ = #{id}
   </delete>

   <insert id="insertCategoryAndBrand">
      insert into category_brand_(
         category_id_,
         brand_id_
      ) values(
         #{categoryId},
         #{brandId}
      )
   </insert>

   <select id="selectCategoryByBrand" resultType="Category">
      SELECT
         a.id_ AS "id",
         a.title_ AS "title",
         a.order_ AS "order",
         a.parent_id_ AS "parentId"
      FROM
         category_ a
         LEFT JOIN category_brand_ b ON b.category_id_ = a.id_
         LEFT JOIN brand_ c ON c.id_ = b.brand_id_
      WHERE
         c.id_ = #{id}
   </select>

</mapper>

4.5.4 业务层

接口

package com.lxs.legou.item.service;

import com.lxs.legou.core.service.ICrudService;
import com.lxs.legou.item.po.Brand;
import com.lxs.legou.item.po.Category;

import java.util.List;

/**
 * @Title: 商品业务对象
 */
public interface IBrandService extends ICrudService<Brand> {

    /**
     * 根据商品id查询分类
     * @param id
     * @return
     */
    public List<Category> selectCategoryByBrand(Long id);

}

实现类

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.dao.BrandDao;
import com.lxs.legou.item.po.Brand;
import com.lxs.legou.item.po.Category;
import com.lxs.legou.item.service.IBrandService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class BrandServiceImpl extends CrudServiceImpl<Brand> implements IBrandService {

   @Override
   @Transactional(readOnly = false)
   public boolean saveOrUpdate(Brand entity) {
      boolean result = super.saveOrUpdate(entity);

      ((BrandDao) getBaseMapper()).deleteCategoryByBrand(entity.getId()); //删除商品和分类的关联

      //添加商品和分类的关联
      Long[] roleIds = entity.getCategoryIds();
      if (null != roleIds) {
         for (Long roleId : roleIds) {
            ((BrandDao) getBaseMapper()).insertCategoryAndBrand(roleId, entity.getId());
         }
      }
      return result;

   }

   @Override
   public List<Category> selectCategoryByBrand(Long id) {
      return ((BrandDao) getBaseMapper()).selectCategoryByBrand(id);
   }

}

注意:这里BaseController中保存调用的是SaveOrUpdate方法,所以保存品牌的逻辑是

  1. 先删除所有品牌和分类关联
  2. 然后再添加品牌和商品的关联
  3. 在Service使用注解@Transactional,控制事务

selectCategoryByBrand方法,后面回显品牌的分类时使用

4.5.5 控制层

package com.lxs.legou.item.controller;

import com.lxs.legou.core.controller.BaseController;
import com.lxs.legou.item.po.Brand;
import com.lxs.legou.item.po.Category;
import com.lxs.legou.item.service.IBrandService;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;


@RestController
@RequestMapping(value = "/brand")
public class BrandController extends BaseController<IBrandService, Brand> {

   @Override
   public void afterEdit(Brand domain) {
      //生成角色列表, 如:1,3,4
      List<Category> categories = service.selectCategoryByBrand(domain.getId());
      Long[] ids = new Long[categories.size()];
      for (int i=0; i< categories.size(); i++) {
         ids[i] = categories.get(i).getId();
      }
      domain.setCategoryIds(ids);
   }

}

afterEdit:是BaseController中加载方法后置回调函数,为前端回显当前品牌的分类列表使用

4.5.6 测试

访问如下地址http://localhost:9005/brand/edit/1528,效果如下:

使用PostMan访问,进行如下访问

5 后台管理前端项目

legou-manager-ui是乐购商城后台管理的前端项目,前端代码不是本系列课程重点,否则我们会一头扎进前端代码的海洋中,出不来了,所以这里只是介绍前端vue代码的实现思路逻辑,具体代码不再从0到1编写,直接拷贝即可,有兴趣的同学可以自行编写

项目是在iview-admin作为基础项目开发,下载iview-admin项目删除不必要的前端组件

前端主要技术栈包括vue、vuex、iview、vue-router、axios、vue-treeselect、vue-table-with-tree-grid、等。。。

前端项目,直接从素材代码直接拷贝前端代码,导入即可

5.0 搭建前端项目

拷贝标准代码,注意暂时把router的导航守卫部分的令牌认证部分代码暂时删除,以后学习了OAuth2认证,再加入讲解

修改axios实例默认的访问地址,执行商品微服务地址

修改后台访问地址

启动测试 npm run dev

5.1 前端基类

5.1.1 base-list.js

base-list.js:前端的列表组件都继承此组件比如品牌列表组件list.vue继承此组件,这样就不用再前端重复的写列表页面相关方法了

import instance from '@/libs/api/index'
import Qs from 'qs'

export const baseList = {

  data () {
    return {
      // 当前路由的子目录/security/post/1 -> security
      namespace: '',
      // 当前路由的最后访问路径/security/post/1-> post
      entityName: '',
      // 初始化信息总条数
      total: 0,
      // 每页显示多少条
      pageSize: 10,
      // 显示的数据
      rows: []
    }
  },
  methods: {
    // 添加
    add () {
      this.$router.push({
        name: `edit_${this.namespace}_${this.entityName}`
      })
    },
    // 删除
    remove (id, index) {
      this.$Modal.confirm({
        title: '确认删除',
        content: '确定要删除吗?',
        onOk: () => {
          instance.get(`/${this.namespace}/${this.entityName}/delete/` + id).then(response => {
            this.$Message.info('删除成功')
            this.query()
          }).catch(error => {
            console.log(error)
          })
        }
      })
    },
    // 批量删除
    removeBatch () {
      if (this.$refs.selection.getSelection().length > 0) {
        this.$Modal.confirm({
          title: '确认删除',
          content: '确定要删除吗?',
          onOk: () => {
            let params = new URLSearchParams()
            this.$refs.selection.getSelection().forEach((o) => {
              params.append('ids', o.id)
            })
            instance.post(`/${this.namespace}/${this.entityName}/delete`, params).then(response => {
              this.$Message.info('删除成功')
              this.query()
            })
          }
        })
      } else {
        this.$Message.info('请选择删除的数据')
      }
    },
    // 修改
    edit (id) {
      this.$router.push({
        name: `edit_${this.namespace}_${this.entityName}`,
        query: { id: id }
      })
    },
    // 查询
    query () {
      instance.post(`/${this.namespace}/${this.entityName}/list-page`, Qs.stringify(this.formData)).then(response => {
        this.rows = response.data.list
        this.total = response.data.total
      }).catch(error => {
        console.log(error)
      })
    },
    // 分页
    changePage (index) {
      this.formData.page = index
      this.query()
    },
    // 设置每页行数
    changePageSize (size) {
      this.formData.page = 1
      this.formData.rows = size
      this.query()
    }
  },
  created () {
    let arrays = this.$route.path.split('/')
    this.namespace = arrays[1]
    this.entityName = arrays[2]
    this.query()
  }
}

具体用法,参考下图

注意这里2个变量

  • namespace:对应模块名
  • entityName:对应模块中管理的实体名称

这两个变量使用如下方法获取

created () {
  let arrays = this.$route.path.split('/')
  this.namespace = arrays[1]
  this.entityName = arrays[2]
  this.query()
}

通过上面代码得出,这两个变量对用路由中的第二个和第三个子目录,品牌路由为/item/brand,得到

  • namespace=item
  • entityName=brand

组件中的所有id和方法调用都与这两个变量有关,这里要特别注意

5.1.2 base-edit.js

base-edit.js:提供了添加和修改页面的所有的方法

import instance from '@/libs/api/index'
import Qs from 'qs'

export const baseEdit = {
    data() {
        return {
            // 当前路由的子目录/security/post/1 -> security
            namespace: '',
            // 当前路由的最后访问路径/security/post/1-> post
            entityName: ''
        }
    },
    methods: {
        /**
         * 模板方法:提交前用来处理保存的数据
         */
        beforeSubmit() {
            alert('b')
        },

        // 提交
        handleSubmit(name) {
            this.$refs[name].validate((valid) => {
                if (valid) {
                    instance.post(`/${this.namespace}/${this.entityName}/save`, Qs.stringify(this.formData, {arrayFormat: 'repeat'})).then(response => {
                        this.$Message.success(response.data.msg);
                        this.go2list()
                    })
                } else {
                    this.$Message.error('Fail!')
                }
            })
        },
        // 重置
        handleReset(name) {
            this.$refs[name].resetFields()
        },
        // 根据ID加载数据
        get(id) {
            instance.get(`/${this.namespace}/${this.entityName}/edit/` + id).then(response => {
                this.formData = Object.assign(response.data);
            }).catch(error => {
                console.log(error)
            })
        },

        go2list() {
            this.$router.push({name: `list_${this.namespace}_${this.entityName}`})
        }
    },

    created() {
        let arrays = this.$route.name.split('_');
        this.namespace = arrays[1];
        this.entityName = arrays[2];

        let id = this.$route.query.id;
        if (id) {
            this.get(id)
        }
    }
}

5.2 前端组件

5.2.1 列表组件

list.vue:品牌列表组件

<template>
    <div>
        <Row>
            <Form ref="formData" :model="formData" :label-width="80">
                <Row style="margin-top: 10px;">
                    <Col span="18">
                    <FormItem label="名称" prop="name">
                    <Input v-model="formData.name" placeholder="名称"></Input>
                    </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'

export default {
  mixins: [baseList],
  data () {
    return {
      formData: {
        name: ''
      },
      columns: [
        {
          type: 'selection',
          width: 60,
          align: 'center'
        },
        {
          title: '名称',
          key: 'name'
        },
        {
          title: '首字母',
          key: 'letter'
        },
        {
          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>

5.2.2 添加修改组件

edit.vue:品牌添加和修改组件

<template>

    <Form ref="form" :model="formData" :rules="ruleValidate" :label-width="80">
        <input type="hidden" v-model="formData.id"/>

        <FormItem label="名称" prop="name">
            <Input v-model="formData.name"></Input>
        </FormItem>
        <FormItem label="首字母" prop="letter">
            <Input v-model="formData.letter"></Input>
        </FormItem>
        <FormItem label="图片" prop="image">
            <single-img :pimage-url="formData.image" @setImgUrl="setImgUrl($event)"></single-img>
        </FormItem>
        <FormItem label="分类" prop="categoryIds">
            <select-categorys v-model="formData.categoryIds"></select-categorys>
        </FormItem>

        <FormItem>
            <Button type="primary" @click="handleSubmit('form')">保存</Button>
            <Button type="primary" @click="go2list()" style="margin-left: 8px">关闭</Button>
        </FormItem>

    </Form>

</template>

<script>
    import {baseEdit} from '@/libs/crud/base-edit'
    import selectCategorys from '_c/select/selectCategorys.vue'
    import singleImg from '_c/upload/singleImg.vue'


    export default {
        components: {selectCategorys, singleImg},
        mixins: [baseEdit],
        data() {
            return {
                formData: {
                    id: '',
                    name: '',
                    letter: '',
                    image: '',
                    categoryIds: []
                },
                ruleValidate: {
                    name: [
                        {required: true, message: '名称不能为空', trigger: 'blur'}
                    ]
                }
            }
        },
        methods: {
            setImgUrl(data) {
                this.formData.image = data;
            }
        }
    }
</script>

5.2.3 选择分类组件

src\components\select\selectCategorys.vue使用vue-treeselect组件实现基于树的选择组件如图

<template>
    <treeselect :value="value" @input="handleInput" :multiple="true" :flat="true" :options="categoryList" :disable-branch-nodes="true" :show-count="true" />
</template>

<script>
    import instance from '@/libs/api/index'
    import { listToTree } from '@/libs/util'
    // import the component
    import Treeselect from '@riophae/vue-treeselect'
    // import the styles
    import '@riophae/vue-treeselect/dist/vue-treeselect.css'

    export default {
        components: { Treeselect },
        name: 'selectCategorys',
        data() {
            return {
                categoryList: []
            }
        },
        props: {value: Array}, // 接收一个 value prop
        methods: {
            handleInput(value) {
                this.$emit('input', value) // 触发 input 事件,并传入新值,v-model:使用:value读,使用@input写
            }
        },
        created() {
            instance.post(`/item/category/list`).then(response => {
                this.categoryList = listToTree(response.data)
            }).catch(error => {
                console.log(error)
            })
        }
    }
</script>
  • :multiple="true":表示多选

自定义事件也可以用于创建支持 v-model 的自定义输入组件。记住:

<input v-model="searchText">

等价于:

<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

所以这里这么写this.$emit('input', value) // 触发 input 事件,并传入新值,v-model:使用:value读,使用@input写

5.3 跨域访问

这里我们先试用注解@CrossOrigin注解设置允许跨域,后期加入spring cloud gateway网关微服务工程,在使用网关配置跨域

6 品牌图片管理

图片存储采用FastDFS,关于FastDfs安装和使用,大家可以参考之前的FastDfs相关课程,这里不再重复

6.1 文件上传微服务

pom.xml

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

    <artifactId>legou-upload</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <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.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

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

        <!-- FastDFS依赖 -->
        <dependency>
            <groupId>com.github.tobato</groupId>
            <artifactId>fastdfs-client</artifactId>
            <version>1.26.7</version>
        </dependency>

    </dependencies>



</project>

bootstrap.yml

spring:
  application:
    name: upload-service

upload-service.yml

server:
  port: 9004

logging:
  #file: demo.log
  pattern:
    console: "%d - %msg%n"
  level:
    org.springframework.web: debug
    com.lxs: debug

security:
  oauth2:
    resource:
      jwt:
        key-uri: http://localhost:9098/oauth/token_key #如果使用JWT,可以获取公钥用于 token 的验签

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB
      max-request-size: 20MB

fdfs:
  # 链接超时
  connect-timeout: 60
  # 读取时间
  so-timeout: 60
  # 生成缩略图参数
  thumb-image:
    width: 150
    height: 150
  tracker-list: 192.168.220.110:22122

启动器

package com.lxs.legou;

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

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class UploadApplication {

    public static void main(String[] args) {
        SpringApplication.run(UploadApplication.class, args);
    }

}

FastDfs配置类

package com.lxs.legou.upload.config;


import com.github.tobato.fastdfs.FdfsClientConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.context.annotation.Import;
import org.springframework.jmx.support.RegistrationPolicy;

@Configuration
@Import(FdfsClientConfig.class)
// Jmx重复注册bean的问题
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class DfsConfig {

}

FastDfs工具类

package com.lxs.legou.upload.config;

import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;

@Component
public class FileDfsUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(FileDfsUtil.class);
    @Resource
    private FastFileStorageClient storageClient ;
    /**
     * 上传文件
     */
    public String upload(MultipartFile multipartFile) throws Exception{
        String originalFilename = multipartFile.getOriginalFilename().
                                  substring(multipartFile.getOriginalFilename().
                                  lastIndexOf(".") + 1);
        StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage(
                              multipartFile.getInputStream(),
                              multipartFile.getSize(),originalFilename , null);
        return storePath.getFullPath() ;
    }

    /**
     * 删除文件
     */
    public void deleteFile(String fileUrl) {
        if (StringUtils.isEmpty(fileUrl)) {
            LOGGER.info("fileUrl == >>文件路径为空...");
            return;
        }
        try {
            StorePath storePath = StorePath.parseFromUrl(fileUrl);
            storageClient.deleteFile(storePath.getGroup(), storePath.getPath());
        } catch (Exception e) {
            LOGGER.info(e.getMessage());
        }
    }
}

Controller

package com.lxs.legou.upload.controller;

import com.lxs.legou.upload.config.FileDfsUtil;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;

@RestController
@CrossOrigin //跨域访问
public class FileController {

    @Resource
    private FileDfsUtil fileDfsUtil ;
    /**
     * http://localhost:7010/swagger-ui.html
     * http://192.168.72.130/group1/M00/00/00/wKhIgl0n4AKABxQEABhlMYw_3Lo825.png
     */
    @ApiOperation(value="上传文件", notes="测试FastDFS文件上传")
    @RequestMapping(value = "/uploadFile",headers="content-type=multipart/form-data", method = RequestMethod.POST)
    public ResponseEntity<String> uploadFile (@RequestParam("file") MultipartFile file){
        String result ;
        try{
            String path = fileDfsUtil.upload(file) ;
            if (!StringUtils.isEmpty(path)){
                result = path ;
            } else {
                result = "上传失败" ;
            }
        } catch (Exception e){
            e.printStackTrace() ;
            result = "服务异常" ;
        }
        return ResponseEntity.ok(result);
    }

    /**
     * 文件删除
     */
    @RequestMapping(value = "/deleteByPath", method = RequestMethod.GET)
    public ResponseEntity<String> deleteByPath (String filePathName){
//        String filePathName = "group1/M00/00/00/wKhjZF3WEDmAPSglAABSZAhj0eU111.jpg" ;
        fileDfsUtil.deleteFile(filePathName);
        return ResponseEntity.ok("SUCCESS") ;
    }

}

使用PostMan测试

6.2 图片上传前端组件

单选图片上传组件src/components/upload/singleImg.vue

<template>
    <div class="demo">
        <div class="demo-upload-list" v-if="hasImage">
            <img :src="imageUrl" />
            <div class="demo-upload-list-cover">
                <Icon type="ios-eye-outline" @click.native="handleView(imageUrl)"></Icon>
                <Icon type="ios-trash-outline" @click.native="handleRemove()"></Icon>
            </div>
        </div>
        <Upload
                :action="actionUrl"
                :format="['jpg','jpeg','png']"
                :max-size="2048"
                :on-exceeded-size="handleMaxSize"
                :on-success="handleSuccess"
                :show-upload-list="false"
                style=" width:58px;">
            <Button icon="ios-cloud-upload-outline">上传图片</Button>
        </Upload>
        <Modal title="图片预览" v-model="visible">
            <img :src="showImageUrl" v-if="visible" style="width: 100%" />
        </Modal>
    </div>

</template>

<script>

    export default {
        name: 'singleImg',
        props: {
            pimageUrl: {
                type: String
            }
        },
        data () {
            return {
                actionUrl: 'http://localhost:9004/uploadFile',
                imageUrl: '',
                hasImage: false,
                showImageUrl: '',
                visible: false
            }
        },
        methods: {
            handleMaxSize (file) {
                this.$Notice.warning({
                    title: '图片大小限制',
                    desc: '文件 ' + file.name + '太大,不能超过 2M.'
                })
            },
            upload () {
                this.loadingStatus = true
                setTimeout(() => {
                    this.file = null
                    this.loadingStatus = false
                    this.$Message.success('Success')
                }, 1500)
            },
            handleView (imageUrl) {
                this.showImageUrl = imageUrl
                this.visible = true
            },
            handleRemove () {
                this.imageUrl = ''
                this.hasImage = false
                this.$emit('setImgUrl', '')
            },
            handleSuccess (res, file) {
                this.imageUrl = `http://192.168.220.110:8080/${res}`;
                this.hasImage = true
            }
        },
        watch: {
            pimageUrl(newVal, oldVal) {
                if (newVal && newVal != oldVal) {
                    this.imageUrl = newVal;
                    this.hasImage = true
                }
            },
            imageUrl(newValue, oldVal) {
                this.$emit('setImgUrl', newValue)
            }
        }
    }

</script>

<style scoped>

    .demo-upload-list {
        display: inline-block;width: 60px;height: 60px;text-align: center;line-height: 60px;
        border: 1px solid transparent;border-radius: 4px;overflow: hidden;background: #fff;
        position: relative;box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);margin-right: 4px;
    }
    .demo-upload-list img {
        width: 100%;height: 100%;
    }
    .demo-upload-list-cover {
        display: none;position: absolute;top: 0;bottom: 0;
        left: 0;right: 0;background: rgba(0, 0, 0, 0.6);
    }
    .demo-upload-list:hover .demo-upload-list-cover {
        display: block;
    }
    .demo-upload-list-cover i {
        color: #fff;font-size: 20px;cursor: pointer;margin: 0 2px;
    }

</style>

品牌添加修改时使用上传组件

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