乐优商城第八天(品牌的管理,图片上传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,在虚拟机上安装

我们使用的客户端

tobato/FastDFS_client

我们使用客户端分为几个常规的步骤

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>

<!-- 设定文件上传的最大值5MB5*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;

}








  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值