乐优商城之品牌新增fastDFS(九)
文章目录
(一)请求处理过程
(二)品牌新增页面分析
(三)品牌新增后台代码
(四)qs工具
(五)页面校验
(六)新增完成后关闭窗口
(七)文件上传代码逻辑
(八)绕过网关访问图片上传并解决跨域问题
(九)fastDFS的介绍
(十)fastDFS的使用
(一)请求处理过程
以品牌查询为例,如下:
(二)品牌新增页面分析
之前完成了品牌的查询,接下来就是新增功能,点击新增品牌按钮,如下:
Brand.vue页面有一个提交按钮,如下:
点击触发addBrand方法,如下:
this.show
就是我们要找的弹窗,如下:
分析该自定义组件中级联选择的实现,如下:
新增品牌后看报错,可以看到请求路径和请求方式,如下:
请求参数如下:
大概的请求信息我们知道了,接下来看回代码,如下:
(三)品牌新增后台代码
这里需要注意的是,品牌和商品分类之间是多对多关系
因此我们有一张中间表,来维护两者间关系,如下:
CREATE TABLE `tb_category_brand` (`category_id` bigint(20) NOT NULL COMMENT '商品类目id',`brand_id` bigint(20) NOT NULL COMMENT '品牌id',PRIMARY KEY (`category_id`,`brand_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品分类和品牌的中间表,两者是多对多关系';
这张表中并没有设置外键约束,似乎与数据库的设计范式不符,为什么这么做?
- 外键会严重影响数据库读写的效率
- 数据删除时会比较麻烦
在电商行业,性能是非常重要的,我们宁可在代码中通过逻辑来维护表关系,也不设置外键
代码如下:
/*** 新增品牌** @param brand* @param cids* @return*/@PostMappingpublic ResponseEntity<Void> saveBrand(@RequestBody Brand brand, @RequestParam("cids") List<Long> cids) {brandService.saveBrand(brand, cids);return ResponseEntity.status(HttpStatus.CREATED).build(); //201,已在服务器上成功创建了一个或多个新资源//如果异常自动抛500}
/*** 新增品牌** @param brand* @param cids*/@Transactionalpublic void saveBrand(Brand brand, List<Long> cids) {brandMapper.insertSelective(brand);//再新增中检表for (Long cid : cids) {//通用mapper操作范围不能超过一张表//所以操作中间表需要我们自己写SQL语句brandMapper.insertCategoryAndBrand(cid, brand.getId());}}
@Insert("insert into tb_category_brand(category_id,brand_id) " +"values(#{cid},#{bid})")void insertCategoryAndBrand(@Param("cid") Long cid, @Param("bid") Long bid);
测试结果如下:
原因:基于JSON数据格式进行传输,所有请求参数会被封装成一个JSON对象
所以在controller不能使用两个对象去接收它,如下:
解决:我们让前端不要传JSON了,传普通的字段,4个参数
(四)qs工具
QS是一个第三方库,我们可以用npm install qs --save
来安装
不过我们在项目中已经集成了,如下:
这个工具的名字:QS,即Query String,请求参数字符串
什么是请求参数字符串?例如: name=jack&age=21
QS工具可以便捷的实现 JS的Object与QueryString的转换
在我们的项目中,QS被注入到了Vue的原型对象中,我们可以通过this.$qs
来获取这个工具:
用法如下:
下面分析qs对象,在控制台中打印,如下:
created() {console.log(this.$qs);}
发现其中有3个方法:
parse()
:把请求参数格式(QueryString)转成JSON格式(Object)stringify()
:把JSON格式(Object)转成请求参数格式(QueryString)
测试一下,使用浏览器工具,把qs对象保存为一个临时变量temp1,然后调用stringify()
方法:
注意:此时后端就不能使用@RequestBody
去接受JSON数据格式了,要去掉
(五)页面校验
在组件后面指定required :rules
,就会开启表单校验,如下:
其中v代表我们输入框输入的内容,如下:
(六)新增完成后关闭窗口
我们无论添加成功还是失败,都要关闭窗口,并且同步数据
这需要窗口子组件通知父组件把自己(子组件)关闭掉,代码流程如下:
(七)文件上传代码逻辑
文件的上传并不只是在品牌管理中有需求,以后的其它服务也可能需要
因此我们创建一个独立的微服务,专门处理各种上传
我们需要EurekaClient和web依赖:
<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies>
覆盖application.yml配置,如下:
server:port: 8082
spring:application:name: upload-serviceservlet:multipart:max-file-size: 5MB #默认是1MB
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eurekainstance:lease-renewal-interval-in-seconds: 5lease-expiration-duration-in-seconds: 15
编写引导类,如下:
@SpringBootApplication
@EnableDiscoveryClient
public class LeyouUploadApplication {public static void main(String[] args) {SpringApplication.run(LeyouUploadApplication.class, args);}
}
编写controller,如下:
@Controller
@RequestMapping("upload")
public class UploadController {@Autowiredprivate UploadService uploadService;@PostMapping("image")public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile file) {String url = uploadService.uploadImage(file);if (StringUtils.isBlank(url)) {return ResponseEntity.badRequest().build();}return ResponseEntity.status(HttpStatus.CREATED).body(url);}
}
编写service,如下:
@Service
public class UploadService {private static final List<String> CONTENT_TYPES = Arrays.asList("image/jpeg", "image/gif");private static final Logger LOGGER = LoggerFactory.getLogger(UploadService.class);public String uploadImage(MultipartFile file) {String originalFilename = file.getOriginalFilename();//校验文件类型String contentType = file.getContentType(); //获取mime类型if (!CONTENT_TYPES.contains(contentType)) {LOGGER.info("文件类型不合法: {}", originalFilename); //{}是占位符,originalFilename会填充占位符return null;}try {//校验文件内容BufferedImage bufferedImage = ImageIO.read(file.getInputStream());if (bufferedImage == null) {LOGGER.info("文件内容不合法: {}", originalFilename);}//保存到文件服务器file.transferTo(new File("G:\\image\\" + originalFilename));//返回url,进行回显return "http://image.leyou.com/" + originalFilename;} catch (IOException e) {LOGGER.info("服务器内部错误: {}", originalFilename);e.printStackTrace();}return null;}
}
配置hosts文件和nginx,如下:
127.0.0.1 image.leyou.com
server {listen 80;server_name image.leyou.com;proxy_set_header X-Forwarded-Host $host;proxy_set_header X-Forwarded-Server $host;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;location /{root G:\\image;}}
用postman测试结果如下:
(八)绕过网关访问图片上传并解决跨域问题
图片上传是文件的传输,如果也经过Zuul网关的代理,文件就会经过多次网路传输,造成网络负担
在高并发时,可能导致网络阻塞,Zuul网关不可用,这样我们的整个系统就瘫痪了
所以,我们上传文件的请求就不经过网关来处理了
我们修改nginx配置,将以/api/upload开头的请求拦截下来,转交到真实的服务地址:
location /api/upload {proxy_pass http://127.0.0.1:8082;proxy_connect_timeout 600;proxy_read_timeout 600;
}
但是这样写显然是不对的,因为ip和端口虽然对了,但是路径没变,依然是:http://127.0.0.1:8002/api/upload/image
,前面多了一个/api
Nginx提供了rewrite指令,用于对地址进行重写,语法规则:rewrite "用来匹配路径的正则" 重写后的路径 [指令];
完整配置如下:
# 上传路径的映射location /api/upload { proxy_pass http://127.0.0.1:8082;proxy_connect_timeout 600;proxy_read_timeout 600;rewrite "^/api/(.*)$" /$1 break; }location / {proxy_pass http://127.0.0.1:10010;proxy_connect_timeout 600;proxy_read_timeout 600;}
首先,我们映射路径是/api/upload,而下面一个映射路径是 / ,根据最长路径匹配原则,/api/upload优先级更高。也就是说,凡是以/api/upload开头的路径,都会被第一个配置处理
proxy_pass
:反向代理,这次我们代理到8082端口,也就是upload-service服务rewrite "^/api/(.*)$" /$1 break
,路径重写:"^/api/(.*)$"
:匹配路径的正则表达式,用了分组语法,把/api/
以后的所有部分当做1组/$1
:重写的目标路径,这里用$1引用前面正则表达式匹配到的分组(组编号从1开始),即/api/
后面的所有。这样新的路径就是除去/api/
以外的所有,就达到了去除/api
前缀的目的break
:指令,常用的有2个,分别是:last、break- last:重写路径结束后,将得到的路径重新进行一次路径匹配
- break:重写路径结束后,不再重新匹配路径。
我们这里不能选择last,否则以新的路径/upload/image来匹配,就不会被正确的匹配到8082端口了
修改完成,输入nginx -s reload
命令重新加载配置
测试发现存在跨域问题,如下:
分析:我们之前是在网关使用过滤器统一解决跨域问题,现在不经过网关就存在跨域问题
我们在upload-service中添加一个CorsFilter即可,如下:
测试结果如下:
(九)fastDFS的介绍
先思考一下,现在上传的功能,有没有什么问题?
上传本身没有任何问题,问题出在保存文件的方式,我们是保存在服务器机器,就会有下面的问题:
- 单机器存储,存储能力有限
- 无法进行水平扩展,因为多台机器的文件无法共享,会出现访问不到的情况
- 数据没有备份,有单点故障风险
- 并发能力差
这个时候,最好使用分布式文件存储来代替本地文件存储
什么是分布式文件系统
分布式文件系统(Distributed File System)是指文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点相连
通俗来讲:
- 传统文件系统管理的文件就存储在本机
- 分布式文件系统管理的文件存储在很多机器,这些机器通过网络连接,要被统一管理
无论是上传或者访问文件,都需要通过管理中心来访问
什么是FastDFS
FastDFS是由淘宝的余庆先生所开发的一个轻量级、高性能的开源分布式文件系统
用纯C语言开发,功能丰富:
- 文件存储
- 文件同步
- 文件访问(上传、下载)
- 存取负载均衡
- 在线扩容
适合有大容量存储需求的应用或系统
同类的分布式文件系统有谷歌的GFS、HDFS(Hadoop)、TFS(淘宝)等
FastDFS的架构
FastDFS两个主要的角色:Tracker Server和Storage Server
- Tracker Server:跟踪服务器,主要负责调度storage节点与client通信
在访问上起负载均衡的作用,记录storage节点的运行状态,是连接client和storage节点的枢纽 - Storage Server:存储服务器,保存文件和文件的meta data(元数据),每个storage server会启动一个单独的线程主动向Tracker cluster中每个tracker server报告其状态信息,包括磁盘使用情况,文件同步情况及文件上传下载次数统计等信息
- Group:文件组,多台Storage Server的集群
上传一个文件到同组内的一台机器上后,FastDFS会将该文件实时同步到同组内的其它所有机器上,起到备份的作用,不同组的服务器,保存的数据不同,而且相互独立,不进行通信 - Tracker Cluster:跟踪服务器的集群,有一组Tracker Server(跟踪服务器)组成
- Storage Cluster:存储集群,由多个Group组成
上传和下载流程
- Client通过Tracker server查找可用的Storage server
- Tracker server向Client返回一台可用的Storage server的IP地址和端口号
- Client直接通过Tracker server返回的IP地址和端口与其中一台Storage server建立连接并进行文件上传
- 上传完成,Storage server返回Client一个文件ID,文件上传结束
下载
- Client通过Tracker server查找要下载文件所在的的Storage server
- Tracker server向Client返回包含指定文件的某个Storage server的IP地址和端口号
- Client直接通过Tracker server返回的IP地址和端口与其中一台Storage server建立连接并指定要下载文件
- 下载文件成功
(十)fastDFS的使用
其实我们一开始就在父工程做了版本管理,之后只需要在子工程引入依赖即可,如下:
修改application.yml,新增内容如下:
fdfs:so-timeout: 1501 # 超时时间connect-timeout: 601 # 连接超时时间thumb-image: # 缩略图width: 60height: 60tracker-list: # tracker地址:你的虚拟机服务器地址+端口(默认是22122)- 192.168.28.233:22122
修改hosts文件,如下:
新增一个Java配置类,如下:
@Configuration
@Import(FdfsClientConfig.class)
// 解决jmx重复注册bean的问题
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class FastClientImporter {}
编写测试用例,如下:
@SpringBootTest
@RunWith(SpringRunner.class)
public class FastDFSTest {@Autowiredprivate FastFileStorageClient storageClient;@Autowiredprivate ThumbImageConfig thumbImageConfig;@Testpublic void testUpload() throws FileNotFoundException {// 要上传的文件File file = new File("G:\\dwrg.jpg");// 上传并保存图片,参数:1-上传的文件流 2-文件的大小 3-文件的后缀 4-可以不管他StorePath storePath = this.storageClient.uploadFile(new FileInputStream(file), file.length(), "jpg", null);// 带分组的路径System.out.println(storePath.getFullPath());// 不带分组的路径System.out.println(storePath.getPath());}@Testpublic void testUploadAndCreateThumb() throws FileNotFoundException {File file = new File("G:\\dwrg.jpg");// 上传并且生成缩略图StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage(new FileInputStream(file), file.length(), "png", null);// 带分组的路径System.out.println(storePath.getFullPath());// 不带分组的路径System.out.println(storePath.getPath());// 获取缩略图路径String path = thumbImageConfig.getThumbImagePath(storePath.getPath());System.out.println(path);}
}
测试结果如下:
最后改造leyou-upload,如下:
乐优商城之品牌新增fastDFS(九)相关推荐
- 乐优商城之分类查询品牌查询(八)
文章目录 (一)编写分类查询 (二)跨域问题 (三)cors跨域原理 (四)解决跨域问题 (五)品牌查询页面分析 (六)品牌查询后台代码 (七)分页查询排序的原理 (八)axios (一)编写分类查询 ...
- 【javaWeb微服务架构项目——乐优商城day03】——(搭建后台管理前端,Vuetify框架,使用域名访问本地项目,实现商品分类查询,cors解决跨域,品牌的查询)
乐优商城day03 0.学习目标 1.搭建后台管理前端 1.1.导入已有资源 1.2.安装依赖 1.3.运行一下看看 1.4.目录结构 1.5.调用关系 2.Vuetify框架 2.1.为什么要学习U ...
- 乐优商城源码/数据库及笔记总结
文章目录 1 源码 2 笔记 2.1 项目概述 2.2 微服务 3 项目优化 4 项目或学习过程中涉及到的设计模式 5 安全问题 6 高内聚低耦合的体现 7 项目中待优化的地方 1 源码 Github ...
- 【javaWeb微服务架构项目——乐优商城day05】——商品规格参数管理(增、删、改,查已完成),SPU和SKU数据结构,商品查询
乐优商城day05 0.学习目标 1.商品规格数据结构 1.1.SPU和SKU 1.2.数据库设计分析 1.2.1.思考并发现问题 1.2.2.分析规格参数 1.2.3.SKU的特有属性 1.2.4. ...
- 乐优商城之项目搭建(四)
文章目录 (一)项目分类 (二)电商行业 (三)专业术语 (四)项目介绍 (五)技术选型 (六)开发环境 (七)搭建后台环境:父工程 (八)搭建后台环境:eureka (九)搭建后台环境:zuul ( ...
- 乐优商城(05)--商品管理
乐优商城(05)–商品管理 一.导入图片资源 现在商品表中虽然有数据,但是所有的图片信息都是无法访问的,因此需要把图片导入到服务器中: 将images.zip文件上传至/leyou/static目录: ...
- 乐优商城笔记六:商品详情页
使用模板引擎 Thymeleaf + nginx 完成商品详情页静态化 完成乐优商城商品详情页 搭建商品详情页微服务 创建子工程 GroupId:com.leyou.service ArtifactI ...
- 乐优商城学习笔记五-商品规格管理
0.学习目标 了解商品规格数据结构设计思路 实现商品规格查询 了解SPU和SKU数据结构设计思路 实现商品查询 了解商品新增的页面实现 独立编写商品新增后台功能 1.商品规格数据结构 乐优商城是一个全 ...
- 乐优商城 Day 09(thymeleaf,Rabbitmq,商品详情页,非教程)
乐优商城学习Day09: 注意:此次代码都是在第八天的基础上 第八天的链接如下: https://blog.csdn.net/zcylxzyh/article/details/100859210 此次 ...
最新文章
- spring4 整合 mybatis3 配置文件
- Docker 集群 图形化显示 Visualizer
- 算法分析与设计-实验三 贪心算法设计
- android struts2 图片上传,xhEditor struts2实现图片上传
- MYSQL 【汇总数据】 【分组数据】 学习记录
- google python代码规范_Python代码这样写才规范优雅! (二)
- python的requests快速上手、高级用法和身份认证
- 3.1 语音的产生与感知
- java list判断是否存在字符串_java怎么判断字符串是否存在于list集合中?
- 21天学通C语言-学习笔记(4)
- 【图像去噪】基于脉冲神经网络PCNN实现图像去噪附matlab代码
- 简单英译汉SQL脚本
- 经纬度转GeoHash
- vue 项目中 zip 压缩包文件下载
- Ubuntu 20.04 Micosoft edg 浏览器安装教程
- 微信小程序的基本操作
- mac地址储存在计算机的内存,mac地址存储在
- Quartz入门教程
- MySQL的时间戳2038年问题还有16年,最好在设计上的时候使用datetime就可以了,不要使用时间戳字段了,即使用了也不要用int类型进行映射,使用long类型映射即可
- Java SE(7)
热门文章
- OSChina 周日乱弹 ——书中自有颜如玉
- Designing Specification
- 推荐21款最佳 HTML5 网页游戏
- 网易互娱耗时最长的活动
- TSP问题解析篇之自适应大邻域搜索(ALNS)算法深度通读(附python代码)
- Unity3D中删除指定路径的文件+删除文件夹
- 产品思维训练 | 你的项目总是不能按期上线,你会如何解决?
- 在linux上运行迷宫问题,C语言 迷宫问题(堆栈及其应用)
- 自己整理的:学习verilog DHL问题笔记——Quartus常见错误
- python读取fnl数据计算200-800km范围内的区域平均、散度、涡度实现grads函数