准备工作
使用nginx,通过域名访问本地项目
- 下载
nginx
压缩包(我这里直接下载的windows包,解压后编写配置文件,即可使用) nginx.conf
...
# 配置两个server节点即可
server {
listen 80;
server_name manage.leyou.com;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://127.0.0.1:9001;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
server {
listen 80;
server_name api.leyou.com;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://127.0.0.1:10001;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
...
跨域问题解决(采用CORS)
- 在
ly-gateway
中配置bean即可
package com.leyou.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
//1.添加CORS配置信息
CorsConfiguration config = new CorsConfiguration();
//1) 允许的域,不要写*,否则cookie就无法使用了
config.addAllowedOrigin("http://manage.leyou.com");
//2) 是否发送Cookie信息
config.setAllowCredentials(true);
//3) 允许的请求方式
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
// 4)允许的头信息
config.addAllowedHeader("*");
// 5) 设置第一次跨域通过后,往后的请求不需要再次发送跨域预请求
config.setMaxAge(7200L);
//2.添加映射路径,我们拦截一切请求
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
//3.返回新的CorsFilter.
return new CorsFilter(configSource);
}
}
创建数据库
-
创建数据库
leyou
-
导入数据库文件
分类管理模块
后端
添加实体类
package com.leyou.pojo;
import lombok.Data;
import tk.mybatis.mapper.annotation.KeySql;
import javax.persistence.Id;
import javax.persistence.Table;
@Table(name="tb_category")
@Data
public class Category {
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
private String name;
private Long parentId;
private Boolean isParent;
private Integer sort;
}
注意:
实体类放到
ly-item-interface
中 实体类中使用的注解需要引入
mapper-core
的依赖。
编写mapper
package com.leyou.item.mapper;
import com.leyou.pojo.Category;
import tk.mybatis.mapper.common.Mapper;
public interface CategoryMapper extends Mapper<Category> {
}
注意:
需要在启动类上加入@MapperScan(com.leyou.item.mapper)注解
编写service
package com.leyou.item.service;
import com.leyou.common.enums.LyExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.item.mapper.CategoryMapper;
import com.leyou.pojo.Category;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
@Service
public class CategoryService {
@Autowired
private CategoryMapper categoryMapper;
/**
* 按分类父ID查询分类列表
* @param pid 父ID
* @return 分类列表
*/
public List<Category> queryCategoryListByPid(Long pid) {
Category category = new Category();
category.setParentId(pid);
List<Category> categoryList = categoryMapper.select(category);
if (CollectionUtils.isEmpty(categoryList)) {
throw new LyException(LyExceptionEnum.CATEGORY_LIST_NOT_FOUND);
}
return categoryList;
}
/**
* 新增分类
* @param category 分类
* @return 分类
*/
public Category addCategory(Category category) {
// 判断其父节点是否为父节点
if (category.getParentId() != CategoryConstans.FIRST_CATEGORY_PARENT_ID) {
Category pCategory = categoryMapper.selectByPrimaryKey(category.getParentId());
if (pCategory != null) {
// 判断是否已经是父菜单
if (!pCategory.getIsParent()) {// 不为父菜单
// 设置为父菜单并保存
pCategory.setIsParent(true);
categoryMapper.updateByPrimaryKey(pCategory);
}
} else {
throw new LyException(LyExceptionEnum.PARENT_CATEGORY_NOT_FOUND);
}
}
if (categoryMapper.insert(category) != 1) {
throw new LyException(LyExceptionEnum.SAVE_FAILURE);
}
return category;
}
/**
* 编辑分类
* @param category 分类
* @return 分类
*/
public Category saveCategory(Category category) {
if (categoryMapper.updateByPrimaryKey(category) != 1) {
throw new LyException(LyExceptionEnum.SAVE_FAILURE);
}
return category;
}
/**
* 删除费雷
* @param categoryId 分类id
* @return 被删除的分类
*/
public Category deleteCategory(Long categoryId) {
Category category = categoryMapper.selectByPrimaryKey(categoryId);
if (category == null) {
throw new LyException(LyExceptionEnum.CATEGORY_NOT_FOUND);
}
// 判断该分类是否为父分类,若为父分类判断分类下是否有子分类
if (category.getIsParent()) {
Category param = new Category();
param.setParentId(categoryId);
if (categoryMapper.select(param).size() > 0) {
throw new LyException(LyExceptionEnum.DELETE_INVALID);
}
}
if (categoryMapper.delete(category) != 1) {
throw new LyException(LyExceptionEnum.SAVE_FAILURE);
}
return category;
}
}
新增异常枚举:
CATEGORY_LIST_NOT_FOUND(404, "未查询到分类列表"), CATEGORY_NOT_FOUND(404, "该分类不存在"), PARENT_CATEGORY_NOT_FOUND(404, "该父分类不存在"), SAVE_FAILURE(500, "保存失败"), DELETE_FAILURE(500, "删除失败"), DELETE_INVALID(500, "该分类下含有子分类,请先删除其子分类"),
新增分类常量类:
package com.leyou.pojo; public final class CategoryConstans { // 分类一级菜单的父ID public static final long FIRST_CATEGORY_PARENT_ID = 0; }
编写controller
package com.leyou.item.cotroller;
import com.leyou.common.enums.LyExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.item.service.CategoryService;
import com.leyou.pojo.Category;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@GetMapping("/list")
public ResponseEntity<List<Category>> queryCategoryListByPid(@RequestParam(value = "pid", defaultValue = "0") Long pid) {
return ResponseEntity.ok(categoryService.queryCategoryListByPid(pid));
}
@PostMapping()
public ResponseEntity<Category> addCategory(@RequestBody Category category) {
if (category == null) {
throw new LyException(LyExceptionEnum.PARAM_CANNOT_BE_NULL);
}
return ResponseEntity.status(HttpStatus.CREATED).body(categoryService.addCategory(category));
}
@PutMapping()
public ResponseEntity<Category> saveCategory(@RequestBody Category category) {
if (category == null) {
throw new LyException(LyExceptionEnum.PARAM_CANNOT_BE_NULL);
}
return ResponseEntity.ok(categoryService.saveCategory(category));
}
@DeleteMapping("/{categoryId}")
public ResponseEntity<Category> deleteCategory(@PathVariable("categoryId") Long categoryId) {
if (categoryId == null) {
throw new LyException(LyExceptionEnum.PARAM_CANNOT_BE_NULL);
}
return ResponseEntity.ok(categoryService.deleteCategory(categoryId));
}
}
前端
Category.vue
<template>
<v-card>
<v-flex xs12 sm10>
<v-tree url="/item/category/list"
:isEdit="isEdit"
@handleAdd="handleAdd"
@handleEdit="handleEdit"
@handleDelete="handleDelete"
@handleClick="handleClick"
/>
</v-flex>
</v-card>
</template>
<script>
export default {
name: "category",
data() {
return {
isEdit:true
}
},
methods: {
handleAdd(node) {
// console.log("add .... ");
console.log(node);
this.$http.post('/item/category', {
"name": node.name,
"sort": node.sort,
"parentId": node.parentId,
"isParent": node.isParent
}).then((response) => {
if (response.status === 201) {
this.$message.info('添加成功');
} else {
this.$message.error(response['message']);
}
}).catch(() => {
this.$message.error('添加失败');
})
},
handleEdit(id, name) {
console.log("edit... id: " + id + ", name: " + name)
// 这个树菜单的编辑,我也没玩明白。23333333
},
handleDelete(id) {
// console.log("delete ... " + id)
this.$http.delete('/item/category/' + id)
.then((response) => {
if (response.status === 200) {
this.$message.info("删除成功")
} else {
this.$message.error(response['message'])
}
})
.catch(() => {
this.$message.error("删除失败")
})
},
handleClick(node) {
}
}
};
</script>
<style scoped>
</style>
树菜单的编辑,我也没玩明白,有玩明白了可以留言一下。
品牌管理模块
后端
添加实体类
package com.leyou.pojo;
import lombok.Data;
import tk.mybatis.mapper.annotation.KeySql;
import javax.persistence.Id;
import javax.persistence.Table;
@Table(name = "tb_brand")
@Data
public class Brand {
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
private String name;
private String image;
private Character letter;
}
编写mapper
package com.leyou.item.mapper;
import com.leyou.pojo.Brand;
import tk.mybatis.mapper.common.Mapper;
public interface BrandMapper extends Mapper<Brand> {
}
编写service
package com.leyou.item.service;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.leyou.common.enums.LyExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.vo.PageResult;
import com.leyou.item.mapper.BrandMapper;
import com.leyou.pojo.Brand;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;
import java.util.List;
@Service
@Transactional
public class BrandService {
@Autowired
private BrandMapper brandMapper;
/**
* 分页查询品牌列表
*
* @param page 当前页码
* @param rows 每页记录数
* @param sortBy 排序字段
* @param desc 是否降序
* @param key 查询关键字
* @return 品牌分页列表
*/
public PageResult<Brand> queryBrandByPage(int page, int rows, String sortBy, Boolean desc, String key) {
// 分页查询
PageHelper.startPage(page, rows);
// 封装查询条件
Example example = new Example(Brand.class);
if (StringUtils.isNoneBlank(key)) {
example.createCriteria().andLike("name", "%" + key + "%");
}
// 排序
if (StringUtils.isNoneBlank(sortBy)) {
example.setOrderByClause(sortBy + (desc ? " DESC" : " ASC"));
}
List<Brand> brands = brandMapper.selectByExample(example);
if (brands.size() < 1) {
throw new LyException(LyExceptionEnum.BRAND_LIST_NOT_FOUND);
}
// 获取分页信息并返回
PageInfo<Brand> pageInfo = new PageInfo<>(brands);
return new PageResult<>(pageInfo.getTotal(), brands);
}
/**
* 新增品牌
*
* @param brand 品牌
* @param categories 品牌所属分类
* @return 品牌
*/
public Brand addBrand(Brand brand, List<Long> categories) {
// 新增品牌
if (brandMapper.insert(brand) != 1) {
throw new LyException(LyExceptionEnum.SAVE_FAILURE);
}
// 添加关联的品牌分类
insertCategoryBrand(brand, categories);
return brand;
}
/**
* 编辑品牌
*
* @param brand 品牌
* @param categories 品牌所属分类
* @return 品牌
*/
public Brand saveBrand(Brand brand, List<Long> categories) {
// 更新品牌
if (brandMapper.updateByPrimaryKeySelective(brand) != 1) {
throw new LyException(LyExceptionEnum.SAVE_FAILURE);
}
// 更新关联的品牌分类
brandMapper.deleteCategoryBrandByBrandId(brand.getId());
insertCategoryBrand(brand, categories);
return brand;
}
/**
* 新增品牌分类关联数据(品牌 1 -> n 分类)
*
* @param brand 品牌
* @param categories 分类
*/
private void insertCategoryBrand(Brand brand, List<Long> categories) {
categories.forEach(categoryId -> {
if (brandMapper.insertCategoryBrand(categoryId, brand.getId()) != 1) {
throw new LyException(LyExceptionEnum.SAVE_FAILURE);
}
});
}
/**
* 删除品牌
*
* @param brandId 品牌ID
* @return 被删除的品牌
*/
public Brand deleteBrand(long brandId) {
// 查询要删除的品牌
Brand brand = brandMapper.selectByPrimaryKey(brandId);
if (brand == null) {
throw new LyException(LyExceptionEnum.BRAND_LIST_NOT_FOUND);
}
// 删除品牌
if(brandMapper.delete(brand) != 1){
throw new LyException(LyExceptionEnum.DELETE_FAILURE);
}
// 删除品牌的关联分类数据
brandMapper.deleteCategoryBrandByBrandId(brandId);
return brand;
}
}
编写controller
package com.leyou.item.cotroller;
import com.leyou.common.enums.LyExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.vo.PageResult;
import com.leyou.item.service.BrandService;
import com.leyou.pojo.Brand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("brand")
public class BrandController {
@Autowired
private BrandService brandService;
@GetMapping("/page")
public ResponseEntity<PageResult<Brand>> queryBrandByPage(@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "rows", defaultValue = "5") int rows,
@RequestParam("sortBy") String sortBy,
@RequestParam(value = "desc",defaultValue = "false") Boolean desc,
@RequestParam("key") String key) {
return ResponseEntity.ok(brandService.queryBrandByPage(page, rows, sortBy, desc, key));
}
@PostMapping
public ResponseEntity<Brand> addBrand(@RequestBody Brand brand,
@RequestParam("categories") List<Long> categories) {
if (CollectionUtils.isEmpty(categories)) {
throw new LyException(LyExceptionEnum.PARAM_INVALID);
}
return ResponseEntity.status(HttpStatus.CREATED).body(brandService.addBrand(brand, categories));
}
@PutMapping
public ResponseEntity<Brand> editBrand(@RequestBody Brand brand,
@RequestParam("categories") List<Long> categories) {
if (CollectionUtils.isEmpty(categories) || brand.getId() == null) {
throw new LyException(LyExceptionEnum.PARAM_INVALID);
}
return ResponseEntity.ok(brandService.saveBrand(brand, categories));
}
@DeleteMapping("/{brandId}")
public ResponseEntity<Brand> deleteBrand(@PathVariable("brandId") long brandId) {
return ResponseEntity.ok(brandService.deleteBrand(brandId));
}
}
文件上传微服务
在品牌管理模块的新增和修改中,需要进行上传图片。所以我们将其抽离出来,形成专门的微服务,以后所有文件上传,无论是图片或者是其他文件,都可以通过此文件上传微服务完成。
搭建项目
创建maven项目
- GroupId:
com.leyou.service
- ArtifactId:
ly-upload
编写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>parent</artifactId>
<groupId>com.leyou</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.service</groupId>
<artifactId>ly-upload</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>ly-common</artifactId>
<version>${leyou.latest.version}</version>
</dependency>
</dependencies>
</project>
启动类
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class LyUpload {
public static void main(String[] args) {
SpringApplication.run(LyUpload.class, args);
}
}
注意在网关中添加文件上传微服务的路由
编写application.yml
server:
port: 8082
spring:
application:
name: upload-service
servlet:
multipart:
# 限制单个文件的大小
max-file-size: 5MB
# Eureka
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9999/eureka
ly:
upload:
imageServer: http://image.leyou.com/
imageTypes:
- image/jpeg
- image/bmp
- image/png
优化
默认情况下,请求通过
Zuul
网关,Spring MVC
会对请求做预处理和缓存,但是文件一旦缓存,会给服务器造成不必要的负担。所以我们需要上传文件的请求不进行缓存。官方给出的解决方案就是在请求的最前面加上/zuul
这样就可以绕开请求的缓存,直接将请求路由到指定的微服务。
配置nginx路径重写
修改nginx.conf
配置文件
分布式文件系统FastDFS
FastDFS简介
参考:http://www.xushuai.fun/2018/12/22/FastDFS简介/
FastDFS安装
参考:http://www.xushuai.fun/2018/12/22/FastDFS安装/
引入FastDFS-client依赖
- java-客户端
-
编辑
pom.xml
文件,新增依赖<dependency> <groupId>com.github.tobato</groupId> <artifactId>fastdfs-client</artifactId> </dependency>
新增配置
fdfs:
so-timeout: 1501
connect-timeout: 601
thumb-image: #缩略图生成参数
width: 150
height: 150
tracker-list: #TrackerList参数,支持多个
- 192.168.136.100:22122
引入配置类
package com.leyou.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;
/**
* 导入FastDFS-Client组件
*
* @author tobato
*
*/
@Configuration
@Import(FdfsClientConfig.class)
// 解决jmx重复注册bean的问题
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class ComponetImport {
// 导入依赖组件
}
编写上传逻辑
-
service
package com.leyou.upload.service; import com.github.tobato.fastdfs.domain.StorePath; import com.github.tobato.fastdfs.service.FastFileStorageClient; import com.leyou.common.enums.LyExceptionEnum; import com.leyou.common.exception.LyException; import com.leyou.upload.constans.FileTypeConstans; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; @Service @Slf4j public class UploadService { @Autowired private FastFileStorageClient fastFileStorageClient; @Autowired private ImageTypeConfigProperty imageTypeConfigProperty; /** * 文件上传 * * @param file 文件 * @return 文件url */ public String uploadFile(MultipartFile file, String fileType) { String url = null; switch (fileType) { case FileTypeConstans.IMAGE: url = uploadImage(file); break; default: throw new LyException(LyExceptionEnum.FILE_TYPE_ERROR); } return url; } /** * 图片上传 * * @param file 图片 * @return 图片url */ private String uploadImage(MultipartFile file) { try { // 校验图片内容,防止修改后缀名恶意上传 BufferedImage image = ImageIO.read(file.getInputStream()); if (image == null || image.getWidth() == 0 || image.getHeight() == 0) { throw new LyException(LyExceptionEnum.FILE_TYPE_ERROR); } // 获取文件后缀名 String filename = file.getOriginalFilename(); String suffix = filename.substring(filename.indexOf(".") + 1); // 上传文件 StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), suffix, null); // 返回文件路径 return imageTypeConfigProperty.getImageServer() + storePath.getFullPath(); } catch (IOException e) { log.error("读取文件内容发生IO异常. e = {}", e); throw new LyException(LyExceptionEnum.READ_FILE_FAILURE); } } }
-
controller
package com.leyou.upload.controller; import com.leyou.common.enums.LyExceptionEnum; import com.leyou.common.exception.LyException; import com.leyou.upload.config.ImageTypeConfigProperty; import com.leyou.upload.constans.FileTypeConstans; import com.leyou.upload.service.UploadService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.util.List; @RestController @RequestMapping("/upload") public class UploadController { @Autowired private UploadService uploadService; @Autowired private ImageTypeConfigProperty imageTypeConfigProperty; @PostMapping("/image") public String uploadImage(@RequestParam("file") MultipartFile file) { // 判断图片类型 if (!imageTypeConfigProperty.getImageTypes().contains(file.getContentType())) { throw new LyException(LyExceptionEnum.FILE_TYPE_ERROR); } return uploadService.uploadFile(file, FileTypeConstans.IMAGE); } }