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

本文涉及的产品
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
服务治理 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的区别

相关文章
|
2月前
|
消息中间件 缓存 监控
优化微服务架构中的数据库访问:策略与最佳实践
在微服务架构中,数据库访问的效率直接影响到系统的性能和可扩展性。本文探讨了优化微服务架构中数据库访问的策略与最佳实践,包括数据分片、缓存策略、异步处理和服务间通信优化。通过具体的技术方案和实例分析,提供了一系列实用的建议,以帮助开发团队提升微服务系统的响应速度和稳定性。
|
2月前
|
消息中间件 监控 开发工具
微服务(三)-实现自动刷新配置(不重启项目情况下)
微服务(三)-实现自动刷新配置(不重启项目情况下)
|
5天前
|
缓存 监控 Java
|
26天前
|
JSON SpringCloudAlibaba Java
Springcloud Alibaba + jdk17+nacos 项目实践
本文基于 `Springcloud Alibaba + JDK17 + Nacos2.x` 介绍了一个微服务项目的搭建过程,包括项目依赖、配置文件、开发实践中的新特性(如文本块、NPE增强、模式匹配)以及常见的问题和解决方案。通过本文,读者可以了解如何高效地搭建和开发微服务项目,并解决一些常见的开发难题。项目代码已上传至 Gitee,欢迎交流学习。
100 1
Springcloud Alibaba + jdk17+nacos 项目实践
|
17天前
|
监控 API 开发者
后端开发中的微服务架构实践与优化
【10月更文挑战第17天】 本文深入探讨了微服务架构在后端开发中的应用及其优化策略。通过分析微服务的核心理念、设计原则及实际案例,揭示了如何构建高效、可扩展的微服务系统。文章强调了微服务架构对于提升系统灵活性、降低耦合度的重要性,并提供了实用的优化建议,帮助开发者更好地应对复杂业务场景下的挑战。
17 7
|
19天前
|
Cloud Native API 持续交付
利用云原生技术优化微服务架构
【10月更文挑战第13天】云原生技术通过容器化、动态编排、服务网格和声明式API,优化了微服务架构的可伸缩性、可靠性和灵活性。本文介绍了云原生技术的核心概念、优势及实施步骤,探讨了其在自动扩展、CI/CD、服务发现和弹性设计等方面的应用,并提供了实战技巧。
|
1月前
|
开发者 Docker 微服务
利用Docker Compose优化微服务架构
在微服务架构中,Docker Compose提供了一种简便有效的方法来定义和运行多容器Docker应用程序,通过YAML文件配置服务、网络和卷,实现一键创建和启动。这不仅确保了开发、测试和生产环境的一致性,还简化了团队协作和维护工作,大幅提升了开发效率。本文将详细介绍Doker Compose的核心优势、基本使用方法及高级功能,帮助你更好地管理和优化微服务架构。
|
15天前
|
存储 Kubernetes 监控
深度解析Kubernetes在微服务架构中的应用与优化
【10月更文挑战第18天】深度解析Kubernetes在微服务架构中的应用与优化
68 0
|
2月前
|
负载均衡 Java 网络架构
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
79 5
|
2月前
|
Java API 对象存储
微服务魔法启动!Spring Cloud与Netflix OSS联手,零基础也能创造服务奇迹!
这段内容介绍了如何使用Spring Cloud和Netflix OSS构建微服务架构。首先,基于Spring Boot创建项目并添加Spring Cloud依赖项。接着配置Eureka服务器实现服务发现,然后创建REST控制器作为API入口。为提高服务稳定性,利用Hystrix实现断路器模式。最后,在启动类中启用Eureka客户端功能。此外,还可集成其他Netflix OSS组件以增强系统功能。通过这些步骤,开发者可以更高效地构建稳定且可扩展的微服务系统。
50 1