乐优商城第八天(品牌的管理,图片上传FastDFS)
乐优商城已经进入第八天了,今天是品牌的管理,品牌要涉及到图片的上传,以及品牌与分类的中间表的维护,这是主要的两个难点。
商品的新增,从0开始新增商品。。
1.首先是页面的入口,一个按钮,点击后应该有弹窗
<!--这里是新增品牌的入口--> <v-btn @click="addBrand" color="primary">新增品牌</v-btn>
点击按钮后,出发addBrand方法,show = true,弹出窗口
methods: { addBrand() { this.brand = {}; this.isEdit = false; this.show = true; }
2.编写弹窗
<v-dialog v-model="show" max-width="600" scrollable v-if="show"> <v-card> <v-toolbar dark dense color="primary"> <v-toolbar-title>{{isEdit ? '修改品牌' : '新增品牌'}}</v-toolbar-title> <v-spacer/> <!--这里是关闭的组件--> <v-btn icon @click="show = false"> <v-icon>close</v-icon> </v-btn> </v-toolbar> <v-card-text class="px-5 py-2"> <!-- 表单 --> <brand-form :oldBrand="brand" :isEdit="isEdit" @close="show = false" :reload="getDataFromApi"/> </v-card-text> </v-card> </v-dialog>
这个是弹窗,其中表单,我们单独写了一个组件,为brandForm
<template> <v-form v-model="valid" ref="brandForm"> <v-text-field label="品牌名称" v-model="brand.name" :rules="[v => !!v || '品牌名称不能为空']" :counter="10" required /> <v-text-field label="首字母" v-model="brand.letter" :rules="[v => v.length === 1 || '首字母只能是1位']" required mask="A" /> <v-cascader url="/item/category/list" required v-model="brand.categories" multiple label="商品分类"/> <v-layout row> <v-flex xs3> <span style="font-size: 16px; color: #444">品牌LOGO:</span> </v-flex> <v-flex> <v-upload v-model="brand.image" url="/item/upload" :multiple="false" :pic-width="250" :pic-height="90" /> </v-flex> </v-layout> <v-layout class="my-4"> <v-btn @click="submit" color="primary">提交</v-btn> <v-btn @click="clear" color="warning">重置</v-btn> </v-layout> </v-form> </template>
3.我们的效果是这样的
把结构拆解
1.
<v-toolbar dark dense color="primary"> <v-toolbar-title>{{isEdit ? '修改品牌' : '新增品牌'}}</v-toolbar-title> <v-spacer/> <!--这里是关闭的组件--> <v-btn icon @click="show = false"> <v-icon>close</v-icon> </v-btn> </v-toolbar>
最上面是一个toobar,里面又两个组件,一个标题,一个关闭按钮
2.
中间表单的内容由外部引入
<v-card-text class="px-5 py-2"> <!-- 表单 --> <brand-form :oldBrand="brand" :isEdit="isEdit" @close="show = false" :reload="getDataFromApi"/> </v-card-text>
:oldBrand = “brand” 是将此页父组件的内容赋值给子组件
子组件用props接收,
props: { oldBrand: {}, isEdit: { type: Boolean, default: false }, show: { type: Boolean, default: true } },
通过监视,赋值给自定义的数据
data() { return { baseUrl: config.api, valid: false, brand: { name: "", image: "", letter: "", categories: [] }, imageDialogVisible: false } },
监控的写法:
watch: { oldBrand: {// 监控oldBrand的变化 deep: true, handler(val) { alert(); /*console.log(1235464654);*/ if (val) { /*console.log(1235464654);*/ // 注意不要直接复制,否则这边的修改会影响到父组件的数据,copy属性即可 this.brand = Object.deepCopy(val) } else { // 为空,初始化brandoldBrand /*console.log(987456123);*/ this.brand = { name: '', letter: '', image: '', categories: [], } } } } },
图片这里是难的点
<v-upload v-model="brand.image" url="/item/upload" :multiple="false" :pic-width="250" :pic-height="90" />
逻辑是,当我们上传完图片后,他会直接提交到item/upload,然后利用双向绑定,将响应的地址绑定到brand.
// 图片上传出成功后操作 handleImageSuccess(res) { this.brand.image = res; },
通过这个函数将img的链接赋值给brand,
当我们点击提交按钮的时候
submit() { // 表单校验 if (this.$refs.brandForm.validate()) { /* this.brand.categories = this.brand.categories.map(c => c.id); this.brand.letter = this.brand.letter.toUpperCase();*/ // 将数据提交到后台 // 2、定义一个请求参数对象,通过解构表达式来获取brand中的属性 const {categories, letter, ...params} = this.brand; // 3、数据库中只要保存分类的id即可,因此我们对categories的值进行处理,只保留id,并转为字符串 params.cids = categories.map(c => c.id).join(","); // 4、将字母都处理为大写 params.letter = letter.toUpperCase(); console.log(params); this.$http({ method: this.isEdit ? 'put' : 'post', url: '/item/brand', data: this.$qs.stringify(params) }).then(() => { // 关闭窗口 this.$message.success("保存成功!"); this.closeWindow(); }).catch(() => { this.$message.error("保存失败!"); }); }
当点击提交的时候,会发送ajax请求
前端页面准备完成后,就可以开始图片的上传了
请求的网址是upload/image,完整的网址是api.leyou.com/api/upload/image
我们的文件上传是不经过zuul的,因为如果经过zull,就会导致静态资源浪费zuul的性能,我们通过nginx代理直接代理到图片上传微服务。所以
第一步,修改nginx的配置,将地址直接代理到微服务,不走网关
location /api/upload {
proxy_pass http://127.0.0.1:8082;
proxy_connect_timeout 600;
proxy_read_timeout 600;
#地址的重写,去掉api
rewrite "^/api/(.*)$" /$1 break;
}
第二步,创建文件上传的微服务
导入web和eureka的依赖
<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>
第三步,配置文件,配置自身和eureka
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:10086/eureka instance: lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳 lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期 prefer-ip-address: true ip-address: 127.0.0.1 instance-id: ${spring.application.name}:${server.port}
编写启动类
@EnableEurekaClient @SpringBootApplication public class leyouUploadApplication { public static void main(String[] args){ SpringApplication.run(Application.class); } }
报了一个错
Caused by: org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getWebServerFactory(ServletWebServerApplicationContext.java:204) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:178) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:152) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]
... 8 common frames omitted
原因是.class写错了
正确的应该是
@EnableEurekaClient @SpringBootApplication public class leyouUploadApplication { public static void main(String[] args){ SpringApplication.run(leyouUploadApplication.class); } }
文件上传的controller
@PostMapping("/image") public ResponseEntity<String> upload(MultipartFile file){ String url = uploadService.upload(file); //如果连接为空,返回错误 if (StringUtils.isBlank(url)){ return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); } return ResponseEntity.status(HttpStatus.OK).body(url); }
文件上传的service
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UploadController.class); //设置允许上传的图片的类型 private static final List<String> suffixes = Arrays.asList("image/png", "image/jpeg","image/gif"); public String upload(MultipartFile file) { //1.验证文件 //验证文件的类型 String contentType = file.getContentType(); if (!suffixes.contains(contentType)) { logger.info("文件上传失败,类型不支持"); return null; } //验证文件的内容 try { BufferedImage bufferedImage = ImageIO.read(file.getInputStream()); if (bufferedImage == null) { logger.info("文件上传失败,不是图片"); return null; } //2.验证成功,保存文件 //首先生成一个文件夹 File dir = new File("D://upload"); if (!dir.exists()) { dir.mkdirs(); } //将图片保存在此文件夹中 file.transferTo(new File(dir, file.getOriginalFilename())); //3.生成url //测试阶段,先生成一个假的url String url = "http://image.leyou.com/upload/" + file.getOriginalFilename(); return url; } catch (IOException e) { e.printStackTrace(); return null; }
当在浏览器端发送图片请求的时候,因为没有经过网关,所以这里又产生了跨域问题,那么我们在upload中单独配置一个过滤器即可。
OPTIONS http://api.leyou.com/api/upload/image 403 ()
upload @ element-ui.common.js?ccbf:25420
post @ element-ui.common.js?ccbf:25261
upload @ element-ui.common.js?ccbf:25200
(anonymous) @ element-ui.common.js?ccbf:25191
uploadFiles @ element-ui.common.js?ccbf:25189
handleChange @ element-ui.common.js?ccbf:25170
invoker @ vue.esm.js?efeb:2027
fn._withTask.fn._withTask @ vue.esm.js?efeb:1826
/#/item/brand:1 Failed to load http://api.leyou.com/api/upload/image: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://manage.leyou.com' is therefore not allowed access. The response had HTTP status code 403.
我们在leyou-upload中单独配置过滤器,跨域的配置
@Configuration public class GlobalCorsConfig { @Bean public CorsFilter corsFilter() { //1.添加CORS配置信息 CorsConfiguration config = new CorsConfiguration(); //1) 允许的域,不要写*,否则cookie就无法使用了 config.addAllowedOrigin("http://manage.leyou.com"); //3) 允许的请求方式 config.addAllowedMethod("OPTIONS"); config.addAllowedMethod("POST"); // 4)允许的头信息 config.addAllowedHeader("*"); //2.添加映射路径,我们拦截一切请求 UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); configSource.registerCorsConfiguration("/**", config); //3.返回新的CorsFilter. return new CorsFilter(configSource); } }
这样图片就上传成功了!!!哈哈哈
接下来,我们要玩更加高大上的东西,fastDFDs,
首先,我们安装fastdfs,在虚拟机上安装
我们使用的客户端
我们使用客户端分为几个常规的步骤
1.导入依赖,父工程中已经管理好了版本
<dependency> <groupId>com.github.tobato</groupId> <artifactId>fastdfs-client</artifactId> </dependency>
2.配置
fdfs: so-timeout: 1501 connect-timeout: 601 thumb-image: # 缩略图 width: 60 height: 60 tracker-list: # tracker地址 - 192.168.56.101:22122
3.编写一个配置类
@Configuration @Import(FdfsClientConfig.class) // 解决jmx重复注册bean的问题 @EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING) public class FastClientImporter { }
4.在service中修改代码,验证还是之间的验证,但是上传的逻辑变了
// 2、将图片上传到FastDFS // 2.1、获取文件后缀名 String extension = StringUtils.substringAfterLast(file.getOriginalFilename(), "."); // 2.2、上传 StorePath storePath = this.storageClient.uploadFile( file.getInputStream(), file.getSize(), extension, null); // 2.3、返回完整路径 return "http://image.leyou.com/" + storePath.getFullPath();
5.如果有缩略图的需要,可以生成缩略图的地址
// 获取缩略图路径
String path = thumbImageConfig.getThumbImagePath(storePath.getPath());
到这里,fastdfs的上传就结束了
当用户上传文件的时候,我们需要返回给他们一个图片保存的链接。用户点击新增会将这个连接上传。这样用户的新增最难的点就实现了。
接下来是增删改查的代码
controller
/** * 商品品牌的查询 * * @param key * @param page * @param rows * @param sortBy * @param desc * @return */ @GetMapping("page") public ResponseEntity<PageResult> queryBrandByPage( @RequestParam("key") String key, @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 ) { PageResult<Brand> pageResult = brandService.queryBrandByPage(key, page, rows, sortBy, desc); if (pageResult == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); } return ResponseEntity.status(HttpStatus.OK).body(pageResult); } /** * 品牌的新增 * @param categories * @param brand * @return */ @PostMapping public ResponseEntity<Void> saveBrand(@RequestParam(value = "cids") List<Long> categories, Brand brand) { this.brandService.saveBrand(categories, brand); return ResponseEntity.status(HttpStatus.CREATED).body(null); } /** * 品牌的修改 * @param categories * @param brand * @return */ @PutMapping public ResponseEntity<Void> updateBrand(@RequestParam(value = "cids") List<Long> categories, Brand brand) { this.brandService.updateBrand(categories, brand); return ResponseEntity.status(HttpStatus.OK).body(null); } /** * 品牌的删除 * @param bid * @return */ @DeleteMapping public ResponseEntity<Void> deleteBrand(@RequestParam(value = "id")Long bid){ this.brandService.deleteBrand(bid); return ResponseEntity.status(HttpStatus.OK).body(null); }
service
/** * 品牌的分页查询 * * @param key * @param page * @param rows * @param sortBy * @param desc * @return */ public PageResult<Brand> queryBrandByPage( String key, int page, int rows, String sortBy, Boolean desc) { //开始分页 PageHelper.startPage(page, rows); //开始过滤 Example example = new Example(Brand.class); if (!StringUtils.isBlank(key)) { example.createCriteria().orLike("name", "%" + key + "%").orLike("letter", key); } if (!StringUtils.isBlank(sortBy)) { String sort = sortBy + (desc ? " asc" : " desc"); example.setOrderByClause(sort); } //分页查询 Page<Brand> pageinfo = (Page<Brand>) brandMapper.selectByExample(example); return new PageResult<Brand>(pageinfo.getTotal(), pageinfo); } /** * 品牌的新增 * * @param categories * @param brand */ @Transactional public void saveBrand(List<Long> categories, Brand brand) { //先增加品牌 try { this.brandMapper.insertSelective(brand); //再维护中间表 for (Long c : categories) { brandMapper.insertCategoryBrand(c, brand.getId()); } } catch (Exception e) { e.printStackTrace(); } } /** * 品牌的修改 * * @param categories * @param brand */ @Transactional public void updateBrand(List<Long> categories, Brand brand) { //修改品牌 brandMapper.updateByPrimaryKeySelective(brand); //维护中间表 for (Long categoryId : categories) { brandMapper.updateCategoryBrand(categoryId, brand.getId()); } } /** * 品牌的删除后 * @param bid */ public void deleteBrand(Long bid) { //删除品牌表 brandMapper.deleteByPrimaryKey(bid); //维护中间表 brandMapper.deleteCategoryBrandByBid(bid); }
mapper:
public interface BrandMapper extends Mapper<Brand> { @Insert("INSERT INTO tb_category_brand (category_id, brand_id) VALUES (#{cid},#{bid})") void insertCategoryBrand(@Param("cid") Long cid, @Param("bid") Long bid); @Update("UPDATE tb_category_brand SET category_id = #{cid} where brand_id = #{bid}" ) void updateCategoryBrand(@Param("cid") Long categoryId, @Param("bid") Long id); @Delete("DELETE from tb_category_brand where brand_id = #{bid}") void deleteCategoryBrandByBid(@Param("bid") Long bid); }
这里注意通用mapper的方法中新增的方法跟通用mapper没有关系,素以我们要在配置文件中重新配置驼峰匹配。
最后,附上几个操作过程的坑
坑一:通用mapper中如果自己写方法的话,需要自己定义驼峰匹配,都是泪啊,坑啊
我以为通用mapper只需要配置@column就可以了,可是@column适用于通用mapper的方法,如果不是通用mapper的救不起作用
mybatis: configuration: map-underscore-to-camel-case: true
坑二:springboot开启事物,也是在启动类的注解上配置的
@EnableTransactionManagement
坑三:最大的坑
数据的回显问题,老师给的代码有一个v-if要用v-show代替,让我决定从此再也不碰前端。
回顾:springmvc的文件上传
第一步.导入依赖,springmvc底层依赖的是file-upload
<!-- 文件上传的依赖 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
第二步,编写解析器
在xxx-serlvet.xml中添加文件上传的解析器
<!-- 定义文件上传解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 设定默认编码 -->
<property name="defaultEncoding" value="UTF-8"></property>
<!-- 设定文件上传的最大值5MB,5*1024*1024 -->
<property name="maxUploadSize" value="5242880"></property>
</bean>
3.controller控制器
// 演示:文件上传
@RequestMapping("/show21")
// 通过参数:MultipartFile file来接收上传的文件,这个是SpringMVC中定义的类,会被自动注入
public ModelAndView show21(@RequestParam("file") MultipartFile file) throws Exception {
ModelAndView mv = new ModelAndView("hello");
if(file != null){
// 将上传得到的文件 转移到指定文件。
file.transferTo(new File("D:/test/" + file.getOriginalFilename()));
}
mv.addObject("msg", "上传成功!" + file.getOriginalFilename());
return mv;
}