【Spring Cloud】新闻头条微服务项目:自媒体前后端搭建&素材管理(含优化)

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 主要介绍自媒体端的前后端搭建及素材管理中的上传图片素材、获取素材列表并展示、收藏素材、删除素材,最后对删除素材做了优化,优化了其逻辑的合理性。

一:前后端搭建

1.后端搭建

后端搭建的模块主要有两个,一个是自媒体端对应的微服务,另外一个是自媒体端对应的网关,见下图:

image.gif编辑

(1)在tbug-headlines-service模块下创建tbug-headlines-wemedia模块,然后创建相关数据库表,数据库表如下:

image.gif编辑

完成上述操作之后添加Nacos配置:

image.gif编辑

配置文件信息如下:

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

image.gif

(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

image.gif

在项目中Nginx作为一级网关,主要用来做反向代理及资源映射,gateway作为二级网关,主要用来做一些拦截校验等功能,关于什么是反向代理以及Nginx和gateway的一些简介可以查看我上一篇文章。

2.前端搭建

由于前面已经配置了app端的代理,这里添加自媒体端的代理之后可以通过nginx的虚拟主机功能使用同一个nginx访问多个项目,见下图

image.gif编辑

实现步骤:

①将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;  #记录代理信息
  }
}

image.gif

二:自媒体素材管理

自媒体端的登录部分我就不做介绍了,因为和前面App端的登录流程大同小异,有需要的可以查看我之前发布的文章。

1.素材上传

(1)需求分析

image.gif编辑

创作者登录后在素材管理那一栏可以看到有上传图片这一选项,点击之后可以从本地选取图片进行上传。

(2)实现流程

image.gif编辑

       这里引入了一个新的功能,后面也会用到,那就是在经过网关认证时候从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>

image.gif

②Nacos配置

在Nacos中添加如下配置(注意填写自己的ip)

minio:
  accessKey: minio
  secretKey: minio123
  bucket: headlines
  endpoint: http://49.34.5.192:9000
  readPath: http://49.24.5.192:9000

image.gif

③创建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);
    }
}

image.gif

④业务层实现类

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);
    }
}

image.gif

2.素材列表查询

(1)需求分析

当创作者点击素材管理时候,会将自己上传过的素材列表都列出来,但是只能显示自己上传的素材信息。

image.gif编辑

除此之外,创作者还能查看自己收藏的素材信息

image.gif编辑

(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;
    }

image.gif

查询条件有三个,一个是查看是否收藏,另一个是根据用户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;
    }
}

image.gif

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("收藏成功");
    }

image.gif

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("删除成功");
    }

image.gif

(2)项目优化

       写完上面的要求之后我想了下发现考虑得还是太简单了,我们应该还要加多一个条件判断,当有文章引用了该素材时候我们就不能将该素材删除,这样应该才是合理的,要不然直接将素材删除的话会造成移动端用户查看文章时候图片无法显示的问题,这样显然很不友好。因此我对删除素材这块做了改进,改进措施如下:

image.gif编辑

实现代码:

@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;
    }

image.gif

       这里需要注意的是不仅要获取文章内容里面的素材信息,还要获取文章封面的素材信息,并把这两者都添加到List中,最后再判断待删除素材是否包含于List中,如果待删除素材在List中则不能被删除。

(3)功能测试

image.gif编辑

首先可以看到,我这里有文章引用了Ump45这张图片素材,下面进行删除测试:

image.gif编辑

image.gif编辑

成功获取到该作者的文章列表并获取到文章列表里面的图片素材信息。

image.gif编辑

成功进入if判断里面,返回错误码

image.gif编辑

image.gif编辑

前端提示素材被引用,至此优化完成,如果你发现有什么待优化的地方可以私信博主。

下篇预告:Nginx与Gateway的区别

相关文章
|
4天前
|
Java 应用服务中间件 Maven
Spring Boot项目打war包(idea:多种方式)
Spring Boot项目打war包(idea:多种方式)
16 1
|
2天前
|
前端开发 JavaScript Java
Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)五(前端页面
Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)五(前端页面
Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)五(前端页面
|
3天前
|
消息中间件 Java 数据安全/隐私保护
Spring Cloud 项目中实现推送消息到 RabbitMQ 消息中间件
Spring Cloud 项目中实现推送消息到 RabbitMQ 消息中间件
|
3天前
|
负载均衡 监控 Java
我把Spring Cloud的超详细资料介绍给你,面试官不会生气吧?geigei
我把Spring Cloud的超详细资料介绍给你,面试官不会生气吧?geigei
|
3天前
|
负载均衡 Java 应用服务中间件
Spring Cloud 负载平衡的意义什么?
负载平衡是指将网络流量在多个服务器之间分布,以达到提高系统性能、增强可靠性和提供更好用户体验的目的。在负载平衡的架构中,多个服务器被组织成一个集群,共同处理用户的请求。
26 4
|
4天前
|
Java 应用服务中间件 Spring
【JavaEE】Spring Boot - 项目的创建和使用(下)
【JavaEE】Spring Boot - 项目的创建和使用
8 0
|
4天前
|
数据可视化 Java 应用服务中间件
【JavaEE】Spring Boot - 项目的创建和使用(上)
【JavaEE】Spring Boot - 项目的创建和使用
9 0
|
4天前
|
存储 Java 对象存储
【JavaEE】DI与DL的介绍-Spring项目的创建-Bean对象的存储与获取
【JavaEE】DI与DL的介绍-Spring项目的创建-Bean对象的存储与获取
9 0
|
4天前
|
Java 关系型数据库 MySQL
【Java Spring开源项目】新蜂(NeeBee)商城项目运行、分析、总结
【Java Spring开源项目】新蜂(NeeBee)商城项目运行、分析、总结
21 4
|
4天前
|
监控 安全 Java
Spring cloud原理详解
Spring cloud原理详解
18 0