我们需要通过云空间来存储讲师头像。在阿里云官网:阿里云-上云就上阿里云 (aliyun.com)注册账号(推荐使用支付宝)并进行实名认证,推荐充值两毛钱,避免因为欠费而被限制功能。在官网开通oss,开通后控制台如下。
点击右下角的创建bucket
。存储类型选择低频访问,读写权限选择公共读,其他建议选择不收费的选项。
再点击文件管理就可以上传文件了。不过,在实际工作中我们一般没有权限直接通过阿里云官网上传文件,而是通过java代码来上传文件。先要获得oss的访问密钥,在官网选择AcessKey->继续使用->创建。
阿里云的产品都有详细的学习资源,我们可以基于此快速进行java代码操作。
在开始写代码前,还需要配置依赖。在后端工程pom文件中之前已经配置了oss相关内容,这里摘录下。
<aliyun-sdk-oss.version>3.1.0</aliyun-sdk-oss.version>
在后端service
模块下创建service_oss
子模块。在这个模块中引入oss
的相关依赖。
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
dependency>
<dependency>
<groupId>joda-timegroupId>
<artifactId>joda-timeartifactId>
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;
结果如下图。
nigix是一个反向代理服务器,主要功能有:
java代码部署到一个服务器,静态资源(图片、网页html等)部署到其它服务器。
可以在官网下载即可(此教程使用windows版本),官网下不了可以通过此链接下载:软件下载 - NGINX开源社区。解压可用。
我们使用cmd执行exe进行启动,注意直接关闭cmd窗口不会关闭nginx,关闭时也需要使用命令。关闭命令如下。
nginx.exe -s stop
我们后端的eduservice
和eduoss
两个模块的端口分别是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,启动前端、两个后端模块。
登录http://localhost:9528/#/login,可以看到现在前端访问的端口已经编程了9001.
(1)ui组件
我们使用现有组件来实现头像上传,我们之前下载使用的vue-admin-template-master
仅仅100多kb,还有一个vue-element-admin-master
有900多kb,功能更加齐全,我们从这个组件的src/components
中找到组件ImageCropper
与PanThumb
,复制到项目的src/components
目录。
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() {
},
如下图。
引入组件。
//引入头像组件
import ImageCropper from '@/components/ImageCropper'
import PanThumb from '@/components/PanThumb'
声明组件。
export default {
//声明引入的组件
components:{ImageCropper,PanThumb},
...
}
启动前后端与ngnix,效果如下。
(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,好看不。
不过上传头像还有个小bug,当上传成功后,再点更改头像,显示的界面是这样的。
只有叉掉重点才会出现正常的页面,可恶,解决它!我们可以让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
}
...
}
下面实现课程分类模块。
(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
字段,这是因为我们对课程进行了二级分类。
如何实现课程的添加呢?我们希望能够批量导入,比如有一个excel表格,我们可以从里面读取数据存到数据库中,这里我们需要借助easyexcel实现我们的功能。
在easyexcel
之前的方案实现对excel的操作,当数据量很大时,相当耗内存。
(1)写操作
在service_edu
引入easyexcel依赖。
<dependency>
<groupId>com.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>2.1.1version>
dependency>
easyexcel
依赖poi
,不过在之前我们已经在service
引入该依赖了,这里贴下。
<dependency>
<groupId>org.apache.poigroupId>
<artifactId>poiartifactId>
dependency>
<dependency>
<groupId>org.apache.poigroupId>
<artifactId>poi-ooxmlartifactId>
dependency>
建立与excel对应的实体类,如下图。
//设置表头和添加的数据字段
@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();
}
控制台输出如下。
添加课程的思路很简单:上传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 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进行测试即可。