一、项目介绍
1、网站介绍
我的项目是一个在线教育网站,分为前台用户系统和后台管理系统;前台用户系统面向学员用户,后台管理系统面向网站运营管理员。
前台用户系统主要功能包括:
- 1、通过课程列表获取课程的价格、讲师、介绍、章节等详情信息,点击章节可以播放课程视频。
- 2、通过讲师列表查找讲师的教的课程和本人介绍、评级等信息。
后台管理系统主要功能包括:
- 课程管理模块,包括课程的上传和删除、课程的分类
- 讲师管理模块,包括讲师信息的添加和删除
- 统计分析模块,统计分析课程播放量、订单量等信息
- 订单管理模块,
- 权限管理模块,
2、技术栈
后端:springboot、springcloud、Mybatis-Plus、springsecurity、redis、nginx、jwt、cannal、Nacos
前端:vue、element-ui
使用到的API:阿里云oss对象存储服务、阿里云视频点播服务、阿里云短信服务、微信支付
3、项目模块
4、系统架构
前后端分离的架构
二、数据库设计
- 使用MySQL数据库
1、表的设计规范
表的设计依据:《阿里巴巴Java开发手册》中规定的
每张表都有:主键id、is_deleted逻辑删除(tinyint类型,01逻辑删除)、gmt_create创建时间(datetime类型)、gmt_modified(datetime类型)更新时间 这几个字段。
如果使用分库分表集群部署,则id类型为verchar,非自增,业务中使用分布式id生成器
2、各表之间的关系
3、课程分类——表的设计
- 课程的 一级分类和二级分类 都放在一张表中
表中有parent_id、id、分类名字段,如果parent_id为0,说明是一级分类。二级分类的parent_id是对应的一级分类的id,这样完成了课程的一级分类和二级分类之间的关系。(给面试官讲的时候要把具体的一级二级分类有哪些说出来,要不然面试官容易听不懂!)
三、跨域的解决方案
1、什么是跨域
跨域是因为浏览器的同源策略引起的,当一个请求的url的协议、域名、端口任意一个与当前页面的url不同就是跨域
2、解决方案一
- 在Controller类上添加注解
@CrossOrigin
-
@RestController @CrossOrigin //解决跨域问题 @RequestMapping("/eduservice/edu-teacher") public class EduTeacherController {}
3、解决方案二
- springcloud的解决方案
四、Swagger
- 前后端分离开发模式中,api文档是最好的前后端沟通方式,通过Swagger生成api文档。
1、Swagger的配置
- common包中编写一个Swagger的配置类
2、Swagger的使用
- 在其他模块使用时,先引入依赖
<dependencies>
<dependency>
<artifactId>service-base</artifactId>
<groupId>com.achang</groupId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- 在controller层的接口类和方法上使用注解:@Api、@ApiOperation,注解中写入接口的说明
五、接口返回统一的数据格式
- 项目中我们会将接口的响应封装成json返回,一般我们会将所有接口的数据格式统一, 使前端(iOS Android,Web)对数据的操作更一致、轻松。
- 一般情况下,统一返回数据格式没有固定的格式,只要能描述清楚返回的数据状态以及要返回的具体数据就可以。
- 但是一般会包含状态码、返回消息、数据这几部分内容。
1、接口返回的统一数据格式
{
"success": 布尔, //响应是否成功
"code": 数字, //响应码
"message": 字符串, //返回的文字消息
"data": HashMap //返回数据,放在键值对中
}
2、统一封装结果的类——R
- R来自common的common_utils
- 返回的data用Map数据结构来存放
-
@Data public class R { @ApiModelProperty("是否成功") private boolean success; @ApiModelProperty("响应码") private Integer code; @ApiModelProperty("返回信息") private String message; @ApiModelProperty("返回数据") private Map<String, Object> data = new HashMap<String, Object>(); //无参构造方法私有 private R() { } //成功 静态方法 public static R ok(){ R r = new R(); r.setSuccess(true); r.setCode(ResultCode.SUCCESS); r.setMessage("成功。。。"); return r; } //失败 静态方法 public static R error(){ R r = new R(); r.setSuccess(false); r.setCode(ResultCode.ERROR); r.setMessage("失败"); return r; } public R success(Boolean success){ this.setSuccess(success); return this; } public R code(Integer code){ this.setCode(code); return this; } public R message(String message){ this.setMessage(message); return this; } public R data(String key,Object value){ this.data.put(key,value); return this; } public R data(Map<String,Object> map){ this.setData(map); return this; } }
3、其他模块的使用
- 添加依赖即可导入R
<dependency> <groupId>com.achang</groupId> <artifactId>common-utils</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
六、阿里云OSS对象云存储服务
- 为了解决海量数据存储与弹性扩容,项目中我们采用云存储的解决方案 - 阿里云 OSS
1、阿里云设置
- 开通阿里云OSS对象存储服务
- 创建一个Bucket存储空间
- 获取阿里云OSS许可证(id和密钥),用于访问OSS存储空间
2、使用OSS对象存储服务
- 在pom.xml中添加阿里云OSS服务的依赖
- 在 后台管理系统 讲师管理模块 添加讲师 时有一个 上传头像 按钮,会请求这个接口,接口返回一个存储头像的url:
@Api(description="阿里云文件管理")
@CrossOrigin //跨域
@RestController
@RequestMapping("/edu_oss/fileoss")
public class OssController {
@Autowired
private OssService ossService;
//上传头像
@ApiOperation(value = "文件上传")
@PostMapping("/upload")
public R uploadOssFile(@RequestParam("file") MultipartFile file){
//获取上传的文件
//返回上传到oss的路径
String url = ossService.uploadFileAvatar(file);
//返回r对象
return R.ok().data("url",url).message("文件上传成功");
}
}
- 根据存储空间(Bucket)的 地址、bucket名、id和密钥 即可完成存储过程,并得到存储的url
@Service
public class OssServiceImpl implements OssService{
//上传文件到阿里云oss
@Override
public String uploadFileAvatar(MultipartFile file){
// yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
String endpoint = ConstandPropertiesUtils.END_POINT;
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = ConstandPropertiesUtils.KEY_ID;
String accessKeySecret = ConstandPropertiesUtils.KEY_SECRET;
String buketName = ConstandPropertiesUtils.BUCKET_NAME;
//获取上传文件的输入流
InputStream inputStream = null;
try {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
inputStream = file.getInputStream();
//获取文件名称
String fileName = file.getOriginalFilename();
//在文件名里添加一个随机唯一的值,否则同名文件上传到阿里云oss会覆盖
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
fileName = uuid + fileName;
//把文件按日期进行分类,也就是说路径上自动添加日期2021/11/27/图片.JPG,会在oss服务器内创建对应文件夹,这样方便按日期管理文件
String datePath = new DateTime().toString("yyyy/MM/dd");//获取当前日期
fileName = datePath + "/" + fileName;
//真实上传oss的文件路径"https://hankong-edu-1010.oss-cn-beijing.aliyuncs.com/2021/11/27/aliyun.PNG90ef72b0ef7a41da9c0fed3d206ff425"
// 依次填写Bucket名称和Object完整路径。Object完整路径中不能包含Bucket名称。
ossClient.putObject(buketName, fileName, inputStream);
// 关闭OSSClient。
ossClient.shutdown();
//返回文件路径
//需要把上传到阿里云oss路径手动拼接出来
//https://achang-edu.oss-cn-hangzhou.aliyuncs.com/default.gif
return "https://"+buketName+"."+endpoint+"/"+fileName;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
七、阿里云视频点播服务——上传视频到阿里云
使用服务端SDK
-
简介:sdk的方式将api进行了进一步的封装,不用自己创建工具类。
- 我们可以基于服务端SDK编写代码来调用点播API,实现对点播产品和服务的快速操作
- 功能介绍:SDK封装了对API的调用请求和响应,避免自行计算较为繁琐的 API签名。支持所有点播服务的API,并提供了相应的示例代码。
VodController
@RestController
@CrossOrigin
@RequestMapping("/eduvod/video")
public class VodController {
@Autowired
private VodService vodService;
//上传视频到阿里云
@PostMapping("/uploadAliyunVideo")
public R uploadAliyunVideo(MultipartFile file){
//返回上传视频的id
String videoId = vodService.uploadVideoAliyun(file);
return R.ok().data("videoId",videoId);
}
}
VodServiceImpl
@Service
public class VodServiceImpl implements VodService {
@Override
public String uploadVideoAliyun(MultipartFile file) {
try {
//accessKeyId,accessKeySecret
//fileName:上传文件原始名称
String fileName = file.getOriginalFilename();
//title:上传之后显示名称
String title = fileName.substring(0,fileName.lastIndexOf("."));
//inputStream:上传文件的输入流
InputStream inputStream = file.getInputStream();
UploadStreamRequest request = new UploadStreamRequest(ConstantVodUtils.ACCESSKEY_ID
, ConstantVodUtils.ACCESSKEY_SECRET
, title, fileName
, inputStream);
UploadVideoImpl uploader = new UploadVideoImpl();
UploadStreamResponse response = uploader.uploadStream(request);
System.out.print("RequestId=" + response.getRequestId() + "\n"); //请求视频点播服务的请求ID
String videoId = null;
if (response.isSuccess()) {
videoId = response.getVideoId();
} else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因
videoId = response.getVideoId();
}
return videoId;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
七、阿里云视频点播服务——视频播放器
- 点击立即播放按钮,播放视频
1、播放方式一:播放地址播放(不推荐)
//播放方式一:此种方式不能播放加密视频
source : '你的视频播放地址',
2、播放方式二:播放凭证播放(推荐)
阿里云播放器支持通过播放凭证自动换取播放地址进行播放,接入方式更为简单,且安全性更高。播放凭证默认时效为100秒(最大为3000秒),只能用于获取指定视频的播放地址,不能混用或重复使用。如果凭证过期则无法获取播放地址,需要重新获取凭证。
- 前端
3、后端
//根据视频id获取视频凭证
@GetMapping("/getPlayAuth/{id}")
public R getPlayAuth(@PathVariable String id){
try {
String playAuth = vodService.getPlayAuth(id);
return R.ok().data("PlayAuth",playAuth);
} catch (Exception e) {
e.printStackTrace();
throw new AchangException(20001,"获取视频凭证失败");
}
}
//根据视频id获取视频凭证
@Override
public String getPlayAuth(String id) {
String accesskeyId = ConstantVodUtils.ACCESSKEY_ID;
String accesskeySecret = ConstantVodUtils.ACCESSKEY_SECRET;
try {
//创建初始化对象
DefaultAcsClient cl = InitObject.initVodClient(accesskeyId,accesskeySecret);
//创建获取视频地址request对象和response对象
GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
//向request对象设置视频id值
request.setVideoId(id);
GetVideoPlayAuthResponse response = cl.getAcsResponse(request);
//获取视频播放凭证
return response.getPlayAuth();
} catch (ClientException e) {
e.printStackTrace();
throw new AchangException(20001,"获取视频凭证失败");
}
}
//初始化类
public class InitObject {
public static DefaultAcsClient initVodClient(String accessKeyId, String accessKeySecret) throws ClientException {
String regionId = "cn-shanghai"; // 点播服务接入区域
DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
DefaultAcsClient client = new DefaultAcsClient(profile);
return client;
}
}
八、项目集成Redis
1、添加redis配置类
@Configuration //配置类
@EnableCaching //开启缓存
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600)) //设置缓存存在的时间 600s
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
2、修改redis.conf文件,在阿里云服务器上启动redis
redis配置文件修改:
- 允许远程连接
- 开机自启动
启动redis:
3、.properties配置文件
spring.redis.host=192.168.44.132
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#spring.redis.password=你设置的redis密码,没有可以不写
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
4、在对应方法上添加redis缓存注解:@Cacheable
- @Cacheable
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在
查询方法
上。
5、哪些数据要缓存
由于首页数据变化不是很频繁,而且首页访问量相对较大,所以我们有必要把首页接口数据缓存到redis缓存中,减少数据库压力和提高访问速度。
主要是首页的幻灯片轮播图和首页中的热门课程数据需要缓存。