【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的区别

相关文章
|
11天前
|
负载均衡 Java API
Java一分钟之-Spring Cloud OpenFeign:声明式服务调用
【6月更文挑战第9天】Spring Cloud OpenFeign是声明式服务调用库,简化了微服务间调用。通过动态代理,它允许开发者用Java接口调用HTTP服务,支持服务发现、负载均衡。本文介绍了OpenFeign的基本概念,展示了如何添加依赖、开启客户端和定义服务接口。还讨论了接口调用失败、超时重试和日志配置等问题及其解决方案,并提供了自定义Feign配置的代码示例。通过学习,读者可以更好地在微服务架构中使用OpenFeign进行服务通信。
169 4
|
12天前
|
消息中间件 负载均衡 Java
Java一分钟之-Spring Cloud:微服务架构工具集
【6月更文挑战第8天】本文介绍了Spring Cloud的核心组件,包括Eureka(服务注册与发现)、Ribbon(客户端负载均衡)、Zuul(API网关)、Hystrix(断路器)、Spring Cloud Config(配置中心)和Spring Cloud Bus(事件总线)。文中强调了各组件的易错点,如Eureka的服务注册失败、Ribbon的配置、Zuul的路由错误、Hystrix的启用及配置、Config Server的加载失败和Bus的通讯问题,并给出了相应的代码示例和解决建议。在实际开发中,关注日志和使用调试工具是保证微服务系统稳定运行的关键。
95 6
|
21小时前
|
Java 开发者 Sentinel
Spring Cloud系列——使用Sentinel进行微服务保护
Spring Cloud系列——使用Sentinel进行微服务保护
12 5
|
2天前
|
机器学习/深度学习 设计模式 人工智能
人工智能和机器学习技术来优化微服务架构
人工智能和机器学习技术来优化微服务架构
12 1
|
2天前
|
监控 前端开发 Java
6个顶级SpringCloud微服务开源项目
6个顶级SpringCloud微服务开源项目
21 5
|
4天前
|
Java 测试技术 持续交付
Java一分钟之-Spring Cloud Contract:契约测试
【6月更文挑战第16天】Spring Cloud Contract是微服务契约测试框架,通过DSL定义接口行为,使用WireMock生成存根进行独立开发验证。常见问题包括契约编写不清晰、未集成到CI/CD和契约版本控制混乱。例如,定义一个`GET /greeting`返回JSON响应的契约,Spring Cloud Contract会自动生成测试代码,帮助确保服务间接口一致性,提升开发效率和系统稳定性。
29 7
|
5天前
|
Java 数据库 开发者
深入解析 Spring Cloud Seata:分布式事务的全面指南
深入解析 Spring Cloud Seata:分布式事务的全面指南
20 1
|
5天前
|
监控 Java API
深入解析 Spring Cloud Sentinel:分布式系统流量控制与熔断降级的全面指南
深入解析 Spring Cloud Sentinel:分布式系统流量控制与熔断降级的全面指南
15 0
深入解析 Spring Cloud Sentinel:分布式系统流量控制与熔断降级的全面指南
|
5天前
|
Java Nacos 数据格式
Spring Cloud Nacos 详解:服务注册与发现及配置管理平台
Spring Cloud Nacos 详解:服务注册与发现及配置管理平台
23 3
|
5天前
|
负载均衡 Java API
Spring Cloud Gateway 详解:构建高效的API网关解决方案
Spring Cloud Gateway 详解:构建高效的API网关解决方案
11 0