【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
【Spring Cloud 初探幽】
【Spring Cloud 初探幽】
13 1
|
6天前
|
Java 开发者 微服务
Spring Cloud原理详解
【5月更文挑战第4天】Spring Cloud是Spring生态系统中的微服务框架,包含配置管理、服务发现、断路器、API网关等工具,简化分布式系统开发。核心组件如Eureka(服务发现)、Config Server(配置中心)、Ribbon(负载均衡)、Hystrix(断路器)、Zuul(API网关)等。本文讨论了Spring Cloud的基本概念、核心组件、常见问题及解决策略,并提供代码示例,帮助开发者更好地理解和实践微服务架构。此外,还涵盖了服务通信方式、安全性、性能优化、自动化部署、服务网格和无服务器架构的融合等话题,揭示了微服务架构的未来趋势。
31 6
|
10天前
|
Java Spring
IDEA 创建 SpringCloud项目-多项目方式
IDEA 创建 SpringCloud项目-多项目方式
|
10天前
|
JSON Java Apache
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient
|
10天前
|
负载均衡 Java 开发者
Spring Cloud:一文读懂其原理与架构
Spring Cloud 是一套微服务解决方案,它整合了Netflix公司的多个开源框架,简化了分布式系统开发。Spring Cloud 提供了服务注册与发现、配置中心、消息总线、负载均衡、熔断机制等工具,让开发者可以快速地构建一些常见的微服务架构。
|
11天前
|
Java Docker 微服务
|
12天前
|
消息中间件 Java RocketMQ
Spring Cloud RocketMQ:构建可靠消息驱动的微服务架构
【4月更文挑战第28天】消息队列在微服务架构中扮演着至关重要的角色,能够实现服务之间的解耦、异步通信以及数据分发。Spring Cloud RocketMQ作为Apache RocketMQ的Spring Cloud集成,为微服务架构提供了可靠的消息传输机制。
27 1
|
12天前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo: 微服务通信的高效解决方案
【4月更文挑战第28天】在微服务架构的发展中,服务间的高效通信至关重要。Spring Cloud Dubbo 提供了一种基于 RPC 的通信方式,使得服务间的调用就像本地方法调用一样简单。本篇博客将探讨 Spring Cloud Dubbo 的核心概念,并通过具体实例展示其在项目中的实战应用。
15 2
|
20小时前
|
监控 持续交付 Docker
使用Docker进行微服务架构的最佳实践
【5月更文挑战第10天】本文探讨了使用Docker实施微服务架构的最佳实践。首先,理解微服务架构是拆分小型独立服务的模式,借助Docker实现快速部署、高可移植性和环境一致性。Docker的优势在于服务扩展、容器编排、自动化构建与部署。最佳实践包括:定义清晰服务边界,使用Dockerfile和Docker Compose自动化构建,利用Docker Swarm或Kubernetes编排,实施服务发现和负载均衡,监控与日志记录,以及持续集成和持续部署。Docker虽重要,但需与其他技术结合以确保系统整体稳定性。