一:前后端搭建
1.后端搭建
后端搭建的模块主要有两个,一个是自媒体端对应的微服务,另外一个是自媒体端对应的网关,见下图:
编辑
(1)在tbug-headlines-service模块下创建tbug-headlines-wemedia模块,然后创建相关数据库表,数据库表如下:
编辑
完成上述操作之后添加Nacos配置:
编辑
配置文件信息如下:
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/headlines_wemedia?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false username: root password: root # 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置 mybatis-plus: mapper-locations: classpath*:mapper/*.xml # 设置别名包扫描路径,通过该属性可以给包中的类注册别名 type-aliases-package: com.my.model.media.pojos
(2)创建tbug-headlines-wemedia-gateway网关微服务,Nacos相关配置如下:
spring: cloud: gateway: globalcors: add-to-simple-url-handler-mapping: true corsConfigurations: '[/**]': allowedHeaders: "*" allowedOrigins: "*" allowedMethods: - GET - POST - DELETE - PUT - OPTION routes: # 平台管理 - id: wemedia uri: lb://headlines-wemedia predicates: - Path=/wemedia/** filters: - StripPrefix= 1
在项目中Nginx作为一级网关,主要用来做反向代理及资源映射,gateway作为二级网关,主要用来做一些拦截校验等功能,关于什么是反向代理以及Nginx和gateway的一些简介可以查看我上一篇文章。
2.前端搭建
由于前面已经配置了app端的代理,这里添加自媒体端的代理之后可以通过nginx的虚拟主机功能使用同一个nginx访问多个项目,见下图
编辑
实现步骤:
①将wemedia-web文件放置到一个文件夹下
②在nginx中headlines.conf目录中新增tbug-headlines-wemedia.conf文件
upstream tbug-wemedia-gateway{ server localhost:51602; } server { listen 8802; location / { root D:/headlinesPro/wemedia-web/; index index.html; } location ~/wemedia/MEDIA/(.*) { proxy_pass http://tbug-wemedia-gateway/$1; proxy_set_header HOST $host; # 不改变源请求头的值 proxy_pass_request_body on; #开启获取请求体 proxy_pass_request_headers on; #开启获取请求头 proxy_set_header X-Real-IP $remote_addr; # 记录真实发出请求的客户端IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #记录代理信息 } }
二:自媒体素材管理
自媒体端的登录部分我就不做介绍了,因为和前面App端的登录流程大同小异,有需要的可以查看我之前发布的文章。
1.素材上传
(1)需求分析
编辑
创作者登录后在素材管理那一栏可以看到有上传图片这一选项,点击之后可以从本地选取图片进行上传。
(2)实现流程
编辑
这里引入了一个新的功能,后面也会用到,那就是在经过网关认证时候从token中取出用户id信息,然后存入headers中。在微服务管理端,为了能够方便获取当前登录用户的id,我们会在拦截器中获取headers中的用户id信息并存入ThreadLocal。至于什么是ThreadLocal,简单来说就是里面会单独保存一份自己的数据,而这份数据是不被其他线程共享的,我们在每次发送请求时候都会往这个线程存入用户id,这样只要是在这个线程中我们就可以通过ThreadLocal获得存入的用户信息。
(3)功能实现
①引入file-start依赖:
前面的文章我们已经将MinIO的常用功能封装到了一个模块中,这时候我们只需要在自媒体微服务的pom文件中引入其依赖即可直接使用。
<dependencies> <dependency> <groupId>com.my</groupId> <artifactId>my-file-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
②Nacos配置
在Nacos中添加如下配置(注意填写自己的ip)
minio: accessKey: minio secretKey: minio123 bucket: headlines endpoint: http://49.34.5.192:9000 readPath: http://49.24.5.192:9000
③创建WmMaterialController
package com.my.wemedia.controller.v1; import com.my.file.service.FileStorageService; import com.my.model.common.dtos.ResponseResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api/v1/material") public class WmMaterialController { @Autowired private WmMaterialService wmMaterialService; @Autowired private FileStorageService fileStorageService; /** * 上传图片素材 * @param multipartFile * @return */ @PostMapping("/upload_picture") public ResponseResult uploadPicture(MultipartFile multipartFile){ return wmMaterialService.uploadPicture(multipartFile); } }
④业务层实现类
package com.my.wemedia.service.impl; import com.my.model.common.dtos.ResponseResult; import com.my.model.common.enums.AppHttpCodeEnum; import com.my.model.wemedia.pojos.WmMaterial; import com.my.utils.thread.WmThreadLocalUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.time.LocalDateTime; import java.util.UUID; @Slf4j @Service @Transactional public class WmMaterialServiceImpl extends ServiceImpl<WmMaterialMapper, WmMaterial> implements WmMaterialService { @Autowired private FileStorageService fileStorageService; /** * 上传素材图片 * @param multipartFile * @return */ @Override public ResponseResult uploadPicture(MultipartFile multipartFile) { //1.检查参数 if(multipartFile == null || multipartFile.getSize() == 0 ) { return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); } //2.上传图片到minio String fileName = UUID.randomUUID().toString().replace("-",""); //获取图片后缀 String originalFileName = multipartFile.getOriginalFilename(); String postfix = originalFileName.substring(originalFileName.lastIndexOf(".")); String fileId = null; try { fileId = fileStorageService.uploadImgFile("",fileName + postfix, multipartFile.getInputStream()); log.info("上传图片至minio中,fileId:{}",fileId); } catch (IOException e) { log.error("WmMaterialService上传文件失败!"); e.printStackTrace(); } //3.保存数据到数据库 WmMaterial wmMaterial = new WmMaterial(); wmMaterial.setUserId(WmThreadLocalUtils.getUser().getId()); wmMaterial.setUrl(fileId); wmMaterial.setCreatedTime(LocalDateTime.now()); wmMaterial.setType((short) 0); //存储类型 0表示图片 1表示视频 wmMaterial.setIsCollection((short) 0); //是否收藏 this.save(wmMaterial); return ResponseResult.okResult(wmMaterial); } }
2.素材列表查询
(1)需求分析
当创作者点击素材管理时候,会将自己上传过的素材列表都列出来,但是只能显示自己上传的素材信息。
编辑
除此之外,创作者还能查看自己收藏的素材信息
编辑
(2)代码实现
这里只提供业务实现的代码:
/** * 获取素材列表 * @param wmMaterialDto * @return */ @Override public PageResponseResult findList(WmMaterialDto wmMaterialDto) { //检查参数 wmMaterialDto.checkParam(); //分页查询 IPage<WmMaterial> page = new Page<>(wmMaterialDto.getPage(),wmMaterialDto.getSize()); LambdaQueryWrapper<WmMaterial> lqw = new LambdaQueryWrapper<>(); //查询是否收藏 if(wmMaterialDto.getIsCollection() != null && wmMaterialDto.getIsCollection() == 1) { lqw.eq(WmMaterial::getIsCollection,wmMaterialDto.getIsCollection()); } //根据用户id查询 lqw.eq(WmThreadLocalUtils.getUser().getId() != null,WmMaterial::getUserId,WmThreadLocalUtils.getUser().getId()); //按照时间倒序排序 lqw.orderByDesc(WmMaterial::getCreatedTime); IPage<WmMaterial> iPage = page(page, lqw); //返回结果 PageResponseResult pageResponseResult = new PageResponseResult(wmMaterialDto.getPage(), wmMaterialDto.getSize(), (int) iPage.getTotal()); pageResponseResult.setData(iPage.getRecords()); return pageResponseResult; }
查询条件有三个,一个是查看是否收藏,另一个是根据用户id查询,这里就需要用到ThreadLocal里面的数据信息了,最后我们让素材按照时间倒序进行排序。需要注意的是,这里采用了MP的分页查询,所以需要设置分页拦截器,可以在config包下创建下面的拦截器类:
package com.my.wemedia.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MpConfig { /** * 设置分页拦截器 * @return */ @Bean public MybatisPlusInterceptor pageInterceptor(){ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } }
3. 素材收藏
要实现素材收藏很简单,当用户发起请求时候修改数据库表中的信息即可(取消收藏也是):
/** * 收藏素材 * @param id * @return */ @GetMapping("/collect/{id}") public ResponseResult<String> Collect(@PathVariable Integer id){ //1.参数校验 if(id == null) return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); //2.修改数据库 LambdaUpdateWrapper<WmMaterial> luw = new LambdaUpdateWrapper<>(); luw.eq(WmMaterial::getId,id); luw.set(WmMaterial::getIsCollection,1); wmMaterialService.update(luw); return ResponseResult.okResult("收藏成功"); }
4.删除素材
(1)原始版本
删除素材就需要考虑两点了,因为我们是把素材上传至MinIO中,数据库中仅仅保存素材的基本信息及MinIO中的地址,因此我们不仅要删除数据库中的素材信息,还要将MinIO中的素材删除。可能你会问,不删除MinIO中的不行吗?当然是可以的,但是这样会造成不必要的资源浪费,所以一般建议也是要删除,在file-start中我们已经将删除的方法封装好了,只需要简单调用一下即可。
/** * 删除素材 * @param id * @return */ @GetMapping("/del_picture/{id}") public ResponseResult<String> delPicture(@PathVariable Integer id) { //1.参数校验 if(id == null) return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); //2.删除minio文件 //2.1获取文件路径 LambdaQueryWrapper<WmMaterial> lqw = new LambdaQueryWrapper<>(); lqw.eq(WmMaterial::getId,id); WmMaterial material = wmMaterialService.getOne(lqw); String url = material.getUrl(); //2.2删除minio文件 fileStorageService.delete(url); //3.更新数据库 wmMaterialService.removeById(id); return ResponseResult.okResult("删除成功"); }
(2)项目优化
写完上面的要求之后我想了下发现考虑得还是太简单了,我们应该还要加多一个条件判断,当有文章引用了该素材时候我们就不能将该素材删除,这样应该才是合理的,要不然直接将素材删除的话会造成移动端用户查看文章时候图片无法显示的问题,这样显然很不友好。因此我对删除素材这块做了改进,改进措施如下:
编辑
实现代码:
@Autowired private WmNewsService wmNewsService; /** * 删除素材 * @param id * @return */ @Override public ResponseResult deleteMaterial(Integer id) { //参数校验 if(id == null) return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); log.info("请求删除素材..."); //1.获取创作者id Integer userId = WmThreadLocalUtils.getUser().getId(); //2.获取该作者所有作品 LambdaQueryWrapper<WmNews> lqw = new LambdaQueryWrapper<>(); lqw.eq(WmNews::getUserId,userId); List<WmNews> wmNewList = wmNewsService.list(lqw); //3.获取作品所有素材信息 List<String> materialList = getNewsImage(wmNewList); //4.获取待删除素材地址 LambdaQueryWrapper<WmMaterial> lqw1 = new LambdaQueryWrapper<>(); lqw1.eq(WmMaterial::getId,id); WmMaterial material = this.getOne(lqw1); String url = material.getUrl(); //5.文章中引用了该素材,不能删除 if(materialList.contains(url)) { log.info("素材被其它文章引用,不能删除!"); return ResponseResult.errorResult(AppHttpCodeEnum.MATERIAL_REFERENCED); } //6.图片素材未被引用,可以删除 //6.1删除minio文件 fileStorageService.delete(url); log.info("成功删除MinIO中素材信息"); //6.2更新数据库 this.removeById(id); log.info("删除数据库中素材信息"); return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS); } /** * 获取文章中图片素材信息 * @param wmNewList * @return */ private List<String> getNewsImage(List<WmNews> wmNewList) { //存储图片链接 List<String> imageUrlList = new ArrayList<>(); for (WmNews wmNews : wmNewList) { //1.从文章内容中提取文本及图片 if(StringUtils.isNotBlank(wmNews.getContent())) { List<Map> maps = JSON.parseArray(wmNews.getContent(),Map.class); for (Map map : maps) { if(map.get("type").equals("image")) { //图片 imageUrlList.add((String) map.get("value")); } } } //2.从封面中提取图片 if(StringUtils.isNotBlank(wmNews.getImages())) { Collections.addAll(imageUrlList, wmNews.getImages().split(",")); } } //3.结果返回 return imageUrlList; }
这里需要注意的是不仅要获取文章内容里面的素材信息,还要获取文章封面的素材信息,并把这两者都添加到List中,最后再判断待删除素材是否包含于List中,如果待删除素材在List中则不能被删除。
(3)功能测试
编辑
首先可以看到,我这里有文章引用了Ump45这张图片素材,下面进行删除测试:
编辑
编辑
成功获取到该作者的文章列表并获取到文章列表里面的图片素材信息。
编辑
成功进入if判断里面,返回错误码
编辑
编辑
前端提示素材被引用,至此优化完成,如果你发现有什么待优化的地方可以私信博主。
下篇预告:Nginx与Gateway的区别