谷粒学院day6 头像上传模块与课程分类模块

day6 头像上传模块与课程分类模块
1.阿里云oss

我们需要通过云空间来存储讲师头像。在阿里云官网:阿里云-上云就上阿里云 (aliyun.com)注册账号(推荐使用支付宝)并进行实名认证,推荐充值两毛钱,避免因为欠费而被限制功能。在官网开通oss,开通后控制台如下。

image-20211030214245358

点击右下角的创建bucket。存储类型选择低频访问,读写权限选择公共读,其他建议选择不收费的选项。

image-20211030215540242

再点击文件管理就可以上传文件了。不过,在实际工作中我们一般没有权限直接通过阿里云官网上传文件,而是通过java代码来上传文件。先要获得oss的访问密钥,在官网选择AcessKey->继续使用->创建。

image-20211030220745295

阿里云的产品都有详细的学习资源,我们可以基于此快速进行java代码操作。

2.头像上传(后端)

在开始写代码前,还需要配置依赖。在后端工程pom文件中之前已经配置了oss相关内容,这里摘录下。

<aliyun-sdk-oss.version>3.1.0</aliyun-sdk-oss.version>

在后端service模块下创建service_oss子模块。在这个模块中引入oss的相关依赖。

<!-- 阿里云oss依赖 -->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<!--日期工具栏依赖-->
<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
</dependency>

在该模块的resource中创建application.properties.注意将其中oss的配置替换成自己在阿里云官网上对应生成的配置,注意替换时密钥不要复制多了空格。

#服务端口
server.port=8002
#服务名
spring.application.name=service-oss
#环境设置:dev、test、prod
spring.profiles.active=dev
#阿里云 OSS
#不同的服务器,地址不同
aliyun.oss.file.endpoint=your endpoint
aliyun.oss.file.keyid=your keyid
aliyun.oss.file.keysecret=your keysecret
#bucket可以在控制台创建,也可以使用java代码创建
aliyun.oss.file.bucketname=your bucketname

新建启动类。包结构见代码。

package com.wangzhou.oss;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = "com.wangzhou")
public class OssApplication {
    public static void main(String[] args) {
        SpringApplication.run(OssApplication.class, args);
    }
}

启动。遇到如下问题。

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class

这是因为我们在上传头像时不需要使用数据库,没有配置数据库,因此启动项目时去找数据库的配置无法找到。在启动类注解中添加如下属性即可解决。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

oss相关的配置都是固定的值,我们前面把它们写到了配置类中,但是我们在代码中要使用这些值,需要创建一个常量类来读取这些值。

在启动类的同级目录建立utils包,包下建类ConstantPropertisUtils

@Component //将这个类交给spring管理
public class ConstantPropertisUtils implements InitializingBean {
    @Value("${aliyun.oss.file.endpoint}") // spring注解,用于将值注入属性
    private String endpoint;
    @Value("${aliyun.oss.file.keyid}")
    private String keyid;
    @Value("${aliyun.oss.file.keysecret}")
    private String keysecret;
    @Value("${aliyun.oss.file.bucketname}")
    private String bucketname;

    public static String END_POINT;
    public static String KEY_ID;
    public static String KEY_SECRET;
    public static String BUCKET_NAME;

    @Override // 项目启动后,这个bean被实例化后执行该方法,将属性赋值给静态常量,后面在其它类中用这些属性就变得简单了。
    public void afterPropertiesSet() throws Exception {
        KEY_ID=this.keyid;
        KEY_SECRET=this.keysecret;
        END_POINT=this.endpoint;
        BUCKET_NAME=this.bucketname;
    }
}

接下来写下service和controller。编写service的过程不需要刻意记忆,只需要查阅官网文档改写即可。

@Service
public class OssServiceImpl implements OssService {

    @Override
    public String uploadFileAvatar(MultipartFile file) {
        //工具类获取值
        String endpoint = ConstantPropertiesUtils.END_POINT;
        String accessKeyId = ConstantPropertiesUtils.KEY_ID;
        String accessKeySecret = ConstantPropertiesUtils.KEY_SECRET;
        String bucketName = ConstantPropertiesUtils.BUCKET_NAME;

        try {
            // 创建OSS实例
            OSS ossClient = new OSSClientBuilder().build(endpoint,accessKeyId,accessKeySecret);
            // 获取文件的输入流
            InputStream inputStream = file.getInputStream();
            String fileName = file.getOriginalFilename();
            // 调用oss的方法
            ossClient.putObject(bucketName, fileName, inputStream);
            ossClient.shutdown();
            // 拼接url 格式:https://edu-banjiu.oss-cn-hangzhou.aliyuncs.com/default.gif
            String url = "http://"+bucketName+"."+endpoint+"/"+fileName ;
            return url;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }

    }
}
@RestController
@CrossOrigin
@EnableSwagger2
@RequestMapping("eduoss/fileoss")
public class OssController {
    @Autowired
    private OssService ossservice;

    // 上传头像
    @PostMapping("/uploadOssFile")
    public R uploadOssFile(MultipartFile file){
        String url = ossservice.uploadFileAvatar(file);
        return R.ok().data("url", url);
    }
}

在网页输入Swagger UI进行测试。在oss控制台查看文件是否上传成功,请读者自测。

功能实现了,但是还存在问题,请读者设想:

(1)如果多次上传同名文件,岂不是就会出现文件覆盖的情况。在文件名上添加一个随机值。

String uuid = UUID.randomUUID().toString();
fileName = uuid + fileName;

(2)如果文件很多,都在同一目录,也太不方便管理了,因此我们需要分日期对文件进行分文件夹管理。

//org.joda.time.DateTime;
String path = new DateTime().toString("yyyy/MM/dd");
fileName = path + "/" +fileName;

结果如下图。

image-20211101222355716

3.ngnix

nigix是一个反向代理服务器,主要功能有:

  • 请求转发

image-20211102194217319

  • 负载均衡

image-20211102194848729

  • 动静分离

java代码部署到一个服务器,静态资源(图片、网页html等)部署到其它服务器。

可以在官网下载即可(此教程使用windows版本),官网下不了可以通过此链接下载:软件下载 - NGINX开源社区。解压可用。

我们使用cmd执行exe进行启动,注意直接关闭cmd窗口不会关闭nginx,关闭时也需要使用命令。关闭命令如下。

nginx.exe -s stop

我们后端的eduserviceeduoss两个模块的端口分别是8001,8002,为了使前端模块与后端交互更优雅,我们使用nignx实现转发请求功能:前端访问nginx的代理端口,再由nginx根据路径中包含字段(eduservice,eduoss)进行转发到对应的模块端口。

先通过修改nginx.conf实现对nginx的配置。

(1)修改nignx的默认端口,80端口很容易与其它端口发生冲突,我们把它改成81.

server {
    listen   81;
    ...
}

(2)配置转发规则:监听9001端口,当访问端口9001时,根据路径去判断并转发到8081或者8082.其中~表示正则匹配。下面检测的字段一定要与后端的路径一致,否则后面测试会出错。

	server {
        listen       9001;
        server_name  localhost;
		
		location ~ /eduservice/ {
				proxy_pass http://localhost:8001;
        }
        location ~ /eduoss/ {
				proxy_pass http://localhost:8002;
        }

    }

(3)在前端的config/env.dev.js文件修改访问的端口。

 BASE_API: '"http://localhost:9001"',

重启nginx,启动前端、两个后端模块。

image-20211102205522243

登录http://localhost:9528/#/login,可以看到现在前端访问的端口已经编程了9001.

image-20211102210315148

4.头像上传(前端)

(1)ui组件

我们使用现有组件来实现头像上传,我们之前下载使用的vue-admin-template-master仅仅100多kb,还有一个vue-element-admin-master有900多kb,功能更加齐全,我们从这个组件的src/components中找到组件ImageCropperPanThumb,复制到项目的src/components目录。

image-20211103200856025

save.vue中。

<!-- 讲师头像:TODO -->
<!-- 讲师头像 -->
<el-form-item label="讲师头像">
    <!-- 头衔缩略图 -->
    <pan-thumb :image="teacher.avatar" />
    <!-- 文件上传按钮 -->
    <el-button
               type="primary"
               icon="el-icon-upload"
               @click="imagecropperShow = true"
               >更换头像
    </el-button>
    <!--
v-show:是否显示上传组件
:key:类似于id,如果一个页面多个图片上传控件,可以做区分
:url:后台上传的url地址
@close:关闭上传组件
@crop-upload-success:上传成功后的回调 -->
    <image-cropper
                   v-show="imagecropperShow"
                   :width="300"
                   :height="300"
                   :key="imagecropperKey"
                   :url="BASE_API + '/admin/oss/file/upload'"
                   field="file"
                   @close="close"
                   @crop-upload-success="cropSuccess"
                   />
</el-form-item>

在save.vue在对上面ui代码使用到的变量赋初始值。

 imagecropperShow:false, // 头像上传的弹框是否默认打开
 imagecropperKey: 0, // 标识符
 BASE_API: process.env.BASE_API, //从dev.env.js中获取

声明组件绑定的方法。

  close() {

  },
  cropSuccess() {

  },

如下图。

image-20211103204010833

引入组件。

//引入头像组件
import ImageCropper from '@/components/ImageCropper'
import PanThumb from '@/components/PanThumb'

声明组件。

export default {
  //声明引入的组件
  components:{ImageCropper,PanThumb},
    ...
    
}

启动前后端与ngnix,效果如下。

image-20211103210836629

(2)功能实现

先修改前端的url

 <image-cropper
                              v-show="imagecropperShow"
                              :width="300"
                              :height="300"
                              :key="imagecropperKey"
                              :url="BASE_API + 'eduoss/fileoss/uploadOssFile'"  //改为后端接口
                              field="file"
                              @close="close"
                              @crop-upload-success="cropSuccess"
                              />

实现图片上传功能。

methods:{
    ...
	close(){ //关闭上传弹框的方法
      this.imagecropperShow=false;
    },
    cropSuccess(data){ //上传成功的方法
      this.imagecropperShow=false;
      // 组件封装了response.data,这里可以直接用data拿到后端数据
      this.teacher.avatar = data.url
    }
    ...
}

xdm,好看不。

image-20211103212423337

不过上传头像还有个小bug,当上传成功后,再点更改头像,显示的界面是这样的。

image-20211104215821516

只有叉掉重点才会出现正常的页面,可恶,解决它!我们可以让imagecropperKey在一次操作(close/cropsuccess)后自增,这样相当于进行了一个版本控制,修改头像时上传组件会做初始化工作。

methods:{
    ...
	close(){ //关闭上传弹框的方法
      this.imagecropperShow=false;
      //上传组件初始化
      this.imagecropperKey = this.imagecropperKey+1
    },
    cropSuccess(data){ //上传成功的方法
      this.imagecropperShow=false;
      // 组件封装了response.data,这里可以直接用data拿到后端数据
      this.teacher.avatar = data.url
      this.imagecropperKey = this.imagecropperKey+1
    }
    ...
}
5.课程分类
5.1 需求概述

下面实现课程分类模块。

image-20211104220904324

(1)数据库建表

CREATE TABLE `edu_subject` (
  `id` char(19) NOT NULL COMMENT '课程类别ID',
  `title` varchar(10) NOT NULL COMMENT '类别名称',
  `parent_id` char(19) NOT NULL DEFAULT '0' COMMENT '父ID',
  `sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序字段',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='课程科目';

#
# Data for table "edu_subject"
#

INSERT INTO `edu_subject` VALUES ('1178214681118568449','后端开发','0',1,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681139539969','Java','1178214681118568449',1,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681181483010','前端开发','0',3,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681210843137','JavaScript','1178214681181483010',4,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681231814658','云计算','0',5,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681252786178','Docker','1178214681231814658',5,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681294729217','Linux','1178214681231814658',6,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681324089345','系统/运维','0',7,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681353449473','Linux','1178214681324089345',7,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681382809602','Windows','1178214681324089345',8,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681399586817','数据库','0',9,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681428946945','MySQL','1178214681399586817',9,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681454112770','MongoDB','1178214681399586817',10,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681483472898','大数据','0',11,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681504444418','Hadoop','1178214681483472898',11,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681529610242','Spark','1178214681483472898',12,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681554776066','人工智能','0',13,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681584136193','Python','1178214681554776066',13,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681613496321','编程语言','0',14,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178214681626079234','Java','1178214681613496321',14,'2019-09-29 15:47:25','2019-09-29 15:47:25'),('1178585108407984130','Python','1178214681118568449',2,'2019-09-30 16:19:22','2019-09-30 16:19:22'),('1178585108454121473','HTML/CSS','1178214681181483010',3,'2019-09-30 16:19:22','2019-09-30 16:19:22');

注意到上面表中有parentid字段,这是因为我们对课程进行了二级分类。

image-20211104223045577

5.2 easyexcel

如何实现课程的添加呢?我们希望能够批量导入,比如有一个excel表格,我们可以从里面读取数据存到数据库中,这里我们需要借助easyexcel实现我们的功能。

image-20211104223546591

easyexcel之前的方案实现对excel的操作,当数据量很大时,相当耗内存。

(1)写操作

service_edu引入easyexcel依赖。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>2.1.1</version>
</dependency>

easyexcel依赖poi,不过在之前我们已经在service引入该依赖了,这里贴下。

 <!--xls-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
        </dependency>

建立与excel对应的实体类,如下图。

image-20211104230602109

//设置表头和添加的数据字段
@Data
@ToString
public class DemoData {

    //学生序号
    //设置excel表头名称
    @ExcelProperty("学生序号")
    private Integer sno;

    //学生名称
    //设置excel表头名称
    @ExcelProperty("学生姓名")
    private String sname;

}

实现写操作。

public class TestEasyExcel {
    public static void main(String[] args) {
        //实现excel写操作
        //1、设置写入文件夹地址和excel文件名称
        String filename="C:\\DemoData.xlsx";

        //调用easyExcel里面的方法实现写操作
        //参数1:文件名称
        //参数2:对应实体类
        EasyExcel
                .write(filename,DemoData.class)
                .sheet("学生列表")
                .doWrite(getLists());
    }

    //创建方法返回List集合
    private static List<DemoData> getLists(){
        ArrayList<DemoData> list = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            DemoData demoData = new DemoData();
            demoData.setSno(i);
            demoData.setSname("zhou :"+ i);
            list.add(demoData);
        }
        return list;
    }
}

实现读操作。

先创建实体类,与之前的过程一致,只是注解新加一个index属性,方便进行读操作时按照序号排序依次读取。

//设置表头和添加的数据字段
@Data
@ToString
public class DemoData {

    //学生序号
    //设置excel表头名称
    @ExcelProperty(value = "学生序号", index = 0)
    private Integer sno;

    //学生名称
    //设置excel表头名称
    @ExcelProperty(value = "学生姓名", index = 1)
    private String sname;

}

读操作需要一行一行读取,这里需要实现一个监听器来完成。

public class ExcelListener extends AnalysisEventListener<DemoData> {
    @Override
    public void invoke(DemoData demoData, AnalysisContext analysisContext) {
        System.out.println("**" + demoData);
    }

    // 表头
    @Override
    public void invokeHead(Map<Integer, CellData> headMap, AnalysisContext context) {
        System.out.println("Head:" + headMap);
    }
    
    // 读取之后的操作
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }
}

最后测试功能。

public class TestEasyExcel {
    public static void main(String[] args) {
        //1、设置写入文件夹地址和excel文件名称
        String filename="F:\\DemoData.xlsx";

        //2、 调用easyExcel里面的方法实现读操作
        EasyExcel.
                read(filename,DemoData.class, new ExcelListener())
                .sheet()
                .doRead();

    }

控制台输出如下。

image-20211105221353788

5.3 添加课程功能的实现

添加课程的思路很简单:上传excel表格实现课程添加,我们需要做下面这些事。

(1)引入easyexcel依赖

(2)使用代码生成器生成实体类对应的controller、service和,mapper。

CodeGenerator的数据库表改为edu_subject即可。

strategy.setInclude("edu_subject");//根据数据库哪张表生成,有多张表就加逗号继续填写

运行生成代码,如果没有生成试着把CodeGenerator移到启动类同一级目录试试。在生成的controller中添加注解@CrossOrigin解决跨域问题。

(3)编写controller

@RestController
@CrossOrigin
@RequestMapping("/eduservice/edu-subject")
public class EduSubjectController {
    @Autowired
    private EduSubjectService eduSubjectService;

    @PostMapping("/addSubject")
    public R addSubject(MultipartFile file) {
        eduSubjectService.addSubject(file);
        return R.ok();
    }
}

记得在EduSubjectService接口及其实现类中增加addSubject()方法。

(4) 创建实体类

\entity\excel下创建实体类SubjectData

@Data
@ToString
public class SubjectData {

    //一级分类
    @ExcelProperty(index = 0)
    private String oneSubjectName;

    //二级分类
    @ExcelProperty(index = 1)
    private String twoSubjectName;
}

(4)读取excel文件

新建包listner,包下建类SubjectExcelListener

public class SubjectExcelListener extends AnalysisEventListener<SubjectData> {

    //因为SubjectExcelListener不能交给spring进行ioc管理,需要自己手动new,不能注入其他对象
    //不能实现数据库操作

    public EduSubjectService eduSubjectService;

    //有参,传递subjectService用于操作数据库
    public SubjectExcelListener(EduSubjectService eduSubjectService) {
        this.eduSubjectService = eduSubjectService;
    }

    //无参
    public SubjectExcelListener() {
    }

    //读取excel内容,一行一行读取
    @Override
    public void invoke(SubjectData subjectData, AnalysisContext analysisContext) {
        //表示excel中没有数据,就不需要读取了
        if (subjectData==null){
            throw new GuliException(20001,"添加失败");
        }

        //一行一行读取,每次读取有两个值,第一个值一级分类,第二个值二级分类
        //判断是否有一级分类是否重复
        EduSubject existOneSubject = this.existOneSubject(eduSubjectService, subjectData.getOneSubjectName());
        if (existOneSubject == null){ //没有相同的一级分类,进行添加
            existOneSubject = new EduSubject();
            existOneSubject.setParentId("0"); //设置一级分类id值,0代表为一级分类
            existOneSubject.setTitle(subjectData.getOneSubjectName());//设置一级分类名
            eduSubjectService.save(existOneSubject);//给数据库添加一级分类
        }

        //获取一级分类的id值
        String parent_id = existOneSubject.getId();
        //判断是否有耳机分类是否重复
        EduSubject existTwoSubject = this.existTwoSubject(eduSubjectService, subjectData.getTwoSubjectName(), parent_id);
        if (existTwoSubject==null){//没有相同的二级分类,进行添加
            existTwoSubject = new EduSubject();
            existTwoSubject.setParentId(parent_id); //设置二级分类id值
            existTwoSubject.setTitle(subjectData.getTwoSubjectName());//设置二级分类名
            eduSubjectService.save(existTwoSubject);//给数据库添加二级分类
        }

    }


    //判断一级分类不能重复添加
    private EduSubject existOneSubject(EduSubjectService eduSubjectService,String name){
        QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
        wrapper.eq("title",name)
                .eq("parent_id","0");
        EduSubject oneSubject = eduSubjectService.getOne(wrapper);
        return oneSubject;
    }

    //判断二级分类不能重复添加
    private EduSubject existTwoSubject(EduSubjectService eduSubjectService,String name,String parentId){
        QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
        wrapper.eq("title",name)
                .eq("parent_id",parentId);
        EduSubject twoSubject = eduSubjectService.getOne(wrapper);
        return twoSubject;
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }
}

实现service,注意新增了参数eduSubjectService,对应的controller与接口请自行调整

@Service
public class EduSubjectServiceImpl extends ServiceImpl<EduSubjectMapper, EduSubject> implements EduSubjectService {

    //添加课程分类
    @Override
    public void addSubject(MultipartFile file,EduSubjectService eduSubjectService) {
        try {
            //文件输入流
            InputStream is = file.getInputStream();

            //调用方法进行读取
            EasyExcel.read(is, SubjectData.class,new SubjectExcelListener(eduSubjectService))
                    .sheet().doRead();

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

使用swagger进行测试即可。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半旧518

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值