[TOC]
3. 需求分析与架构设计
3.1 需求分析
在docker中启动容器
启动后台服务
注意启动顺序
- Registry
- Config
- auth-center
- legou-security
- 后面微服务没有顺序
启动前台门户
# 进入目录legou-portal-ui,执行如下命令
npm run dev
启动后台管理
# 进入legou-manager-ui
npm run dev
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多线程,模块化,面向切面编程,设计模式,浏览器兼容性,性能优化等等。
我们在本课程中提供与项目课程配套的管理后台的前端代码,但是不深入讲解前端的内容。这样我们会将更多的精力放在后端代码的开发上!
3.2.2 技术架构
3.2.3 系统架构图
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 项目结构说明
结构说明:
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:工具类工程,拷贝即可
项目结构如下:
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/
,效果如下:
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方法,所以保存品牌的逻辑是
- 先删除所有品牌和分类关联
- 然后再添加品牌和商品的关联
- 在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>
品牌添加修改时使用上传组件