乐优商城之品牌新增&fastDFS(九)
文章目录(一)请求处理过程(二)品牌新增页面分析(三)品牌新增后台代码(四)qs工具(五)页面校验(一)请求处理过程以品牌查询为例,如下:(二)品牌新增页面分析之前完成了品牌的查询,接下来就是新增功能,点击新增品牌按钮,如下:Brand.vue页面有一个提交按钮,如下:点击触发addBrand方法,如下:this.show就是我们要找的弹窗,如下:分析该自定义组件中级联选择的实现,如下:新增品牌后
文章目录
(一)请求处理过程
(二)品牌新增页面分析
(三)品牌新增后台代码
(四)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
*/
@PostMapping
public 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
*/
@Transactional
public 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-service
servlet:
multipart:
max-file-size: 5MB #默认是1MB
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
lease-renewal-interval-in-seconds: 5
lease-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 {
@Autowired
private 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: 60
height: 60
tracker-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 {
@Autowired
private FastFileStorageClient storageClient;
@Autowired
private ThumbImageConfig thumbImageConfig;
@Test
public 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());
}
@Test
public 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,如下:
更多推荐
所有评论(0)