【Spring Cloud】新闻头条微服务项目:自媒体文章管理

本文涉及的产品
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 介绍了自媒体创作端文章内容的展示以及文章的发布。

一:获取所有频道

1.需求分析

当我们点击内容管理时候,页面会自动发送请求获取频道列表(Java、MySql、大数据、推荐等),这时候用户可以进行频道的选择以过滤其他频道的文章。

image.gif编辑

2.表结构

image.gif编辑数据库表字段有频道名称、频道描述、是否默认频道、频道状态、默认排序、创建时间,其对应的实体类为:

package com.my.model.wemedia.pojos;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
 * <p>
 * 频道信息表
 * </p>
 *
 * @author itheima
 */
@Data
@TableName("wm_channel")
public class WmChannel implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * 频道名称
     */
    @TableField("name")
    private String name;
    /**
     * 频道描述
     */
    @TableField("description")
    private String description;
    /**
     * 是否默认频道
     * 1:默认     true
     * 0:非默认   false
     */
    @TableField("is_default")
    private Boolean isDefault;
    /**
     * 是否启用
     * 1:启用   true
     * 0:禁用   false
     */
    @TableField("status")
    private Boolean status;
    /**
     * 默认排序
     */
    @TableField("ord")
    private Integer ord;
    /**
     * 创建时间
     */
    @TableField("created_time")
    private Date createdTime;
}

image.gif

3.接口定义

说明
接口路径 /api/v1/channel/channels
请求方式 POST
参数
响应结果 ResponseResult

4.功能实现

实现代码不难,就是简单地从数据库中获取所有频道的信息并返回,为了节省篇幅我这里就不将代码放上来了,可以自己动手实现一下。

二:查询文章

1.需求说明

       在内容列表页面,我们可以通过特定条件筛选文章,比如按照文章的状态、频道、发布时间等筛选出自己想要的文章信息。

image.gif编辑

2.表结构

image.gif编辑        自媒体文章表字段比较多,主要包括用户id、标题、图文内容等一些文章信息,这时候你可能会有这样的疑问,为什么前面移动端是将表格拆分成三份这里不进行拆分。我们首先要明确的是拆分的目的及意义是什么,前面说过拆分是为了减轻数据库压力,减少IO操作,因为移动端用户量是相当大的,而且大多数时候用户只是刷新列表并不用查看文章详情。但是在自媒体创作端则不同,首先用户量不大,其次一般创作者在进行文章管理时候都会对文章进行修改,这时候就需要获取文章详细信息,把这些信息封装成一个表比较好操作,同时数据库压力不会很大。

package com.my.model.wemedia.pojos;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.apache.ibatis.type.Alias;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;
/**
 * <p>
 * 自媒体图文内容信息表
 * </p>
 *
 * @author itheima
 */
@Data
@TableName("wm_news")
public class WmNews implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * 自媒体用户ID
     */
    @TableField("user_id")
    private Integer userId;
    /**
     * 标题
     */
    @TableField("title")
    private String title;
    /**
     * 图文内容
     */
    @TableField("content")
    private String content;
    /**
     * 文章布局
     * 0 无图文章
     * 1 单图文章
     * 3 多图文章
     */
    @TableField("type")
    private Short type;
    /**
     * 图文频道ID
     */
    @TableField("channel_id")
    private Integer channelId;
    @TableField("labels")
    private String labels;
    /**
     * 创建时间
     */
    @TableField("created_time")
    private Date createdTime;
    /**
     * 提交时间
     */
    @TableField("submited_time")
    private Date submitedTime;
    /**
     * 当前状态
     * 0 草稿
     * 1 提交(待审核)
     * 2 审核失败
     * 3 人工审核
     * 4 人工审核通过
     * 8 审核通过(待发布)
     * 9 已发布
     */
    @TableField("status")
    private Short status;
    /**
     * 定时发布时间,不定时则为空
     */
    @TableField("publish_time")
    private Date publishTime;
    /**
     * 拒绝理由
     */
    @TableField("reason")
    private String reason;
    /**
     * 发布库文章ID
     */
    @TableField("article_id")
    private Long articleId;
    /**
     * //图片用逗号分隔
     */
    @TableField("images")
    private String images;
    @TableField("enable")
    private Short enable;
    // 状态枚举类
    @Alias("WmNewsStatus")
    public enum Status {
        NORMAL((short) 0), SUBMIT((short) 1), FAIL((short) 2), ADMIN_AUTH((short) 3), ADMIN_SUCCESS((short) 4), SUCCESS((short) 8), PUBLISHED((short) 9);
        short code;
        Status(short code) {
            this.code = code;
        }
        public short getCode() {
            return this.code;
        }
    }
}

image.gif

3.接口定义

说明
接口路径 /api/v1/news/list
请求方式 POST
参数 WmNewsPageReqDto
响应结果 ResponseResult

WmNewsPageReqDto :

package com.my.model.wemedia.dtos;
import com.my.model.common.dtos.PageRequestDto;
import lombok.Data;
import java.util.Date;
@Data
public class WmNewsPageReqDto extends PageRequestDto {
    /**
     * 状态
     */
    private Short status;
    /**
     * 开始时间
     */
    private Date beginPubDate;
    /**
     * 结束时间
     */
    private Date endPubDate;
    /**
     * 所属频道ID
     */
    private Integer channelId;
    /**
     * 关键字
     */
    private String keyword;
}

image.gif

4.功能实现

package com.my.wemedia.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.my.model.common.dtos.PageResponseResult;
import com.my.model.common.dtos.ResponseResult;
import com.my.model.wemedia.dtos.WmNewsPageReqDto;
import com.my.model.wemedia.pojos.WmNews;
import com.my.utils.thread.WmThreadLocalUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@Transactional
public class WmNewsServiceImpl extends ServiceImpl<WmNewsMapper, WmNews> implements WmNewsService {
    /**
     * 查找文章内容
     * @param dto
     * @return
     */
    @Override
    public ResponseResult findContentList(WmNewsPageReqDto dto) {
        //1.参数检查
        dto.checkParam();
        //2.分页条件查询
        IPage<WmNews> page = new Page<>(dto.getPage(),dto.getSize());
        LambdaQueryWrapper<WmNews> lqw = new LambdaQueryWrapper<>();
        //状态查询
        lqw.eq(dto.getStatus() != null,WmNews::getStatus,dto.getStatus());
        //频道精确查询
        lqw.eq(dto.getChannelId() != null,WmNews::getChannelId,dto.getChannelId());
        //时间范围查询
        if(dto.getBeginPubDate() != null && dto.getEndPubDate() != null) {
            lqw.between(WmNews::getPublishTime,dto.getBeginPubDate(),dto.getEndPubDate());
        }
        //关键字模糊查询
        lqw.eq(dto.getKeyword() != null,WmNews::getContent,dto.getKeyword());
        //查询当前登录人的文章
        lqw.eq(WmNews::getUserId, WmThreadLocalUtils.getUser().getId());
        //按照发布时间倒序排序
        lqw.orderByDesc(WmNews::getPublishTime);
        page = page(page, lqw);
        //3.结果返回
        ResponseResult responseResult = new PageResponseResult(dto.getPage(), dto.getSize(), (int) page.getTotal());
        responseResult.setData(page.getRecords());
        return responseResult;
    }
}

image.gif

三:文章发布

1.需求分析

image.gif编辑

       文章的发布是这个项目的难点之一,因为涉及到文章内容和素材的关系,这种关系又分为内容引用和封面引用两种。当用户选择的是自动设置封面时候,我们需要根据情况选择是设置无封面、单图封面、双图封面、多图封面。在提交部分,创作者可以选择保存为草稿,也可以选择提交审核,审核通过即可发表,此外,创作者还可以选择定时发布文章,不过审核部分和定时发布部分留到后面再说。

2.表结构

除了文章表之外,我们还需要另外两张表,即素材表和素材关系表:

wm_material 素材表

image.gif编辑wm_news_material 文章素材关系表  

image.gif编辑这三张表的关系见下图:

image.gif编辑可以看到文章表、素材表和素材关系表之间的关系都是一对多的关系,因为一篇文章可能包含多张素材,一张素材也可能被多次引用。

3.实现思路

image.gif编辑  

       当创作者点击保存草稿或者提交审核之后,首先应根据文章有无id来判断这是修改还是新增文章,假如有id则说明为修改文章,执行修改操作;若无id表明为新增操作,执行新增操作。然后判断是否为草稿,若为草稿则不需要保存素材和文章图片的关系,因为草稿是不用发布到移动端的,素材关系表是移动端使用到的。

4.代码实现

package com.my.wemedia.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.my.common.constans.WemediaConstants;
import com.my.common.exception.CustomException;
import com.my.model.common.dtos.ResponseResult;
import com.my.model.common.enums.AppHttpCodeEnum;
import com.my.model.wemedia.dtos.WmNewsDto;
import com.my.model.wemedia.pojos.WmMaterial;
import com.my.model.wemedia.pojos.WmNews;
import com.my.model.wemedia.pojos.WmNewsMaterial;
import com.my.utils.thread.WmThreadLocalUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@Transactional
public class WmNewsServiceImpl extends ServiceImpl<WmNewsMapper, WmNews> implements WmNewsService {
    /**
     * 提交文章
     * @param dto
     * @return
     */
    @Override
    public ResponseResult submitNews(WmNewsDto dto) {
        //1.参数校验
        if(dto == null || dto.getContent().length() == 0) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        //2.保存或修改文章
        //2.1属性拷贝
        WmNews wmNews = new WmNews();
        BeanUtils.copyProperties(dto,wmNews);
        //2.2设置封面图片
        if(dto.getImages() != null && dto.getImages().size() != 0) {
            String images = StringUtils.join(dto.getImages(), ",");
            wmNews.setImages(images);
        }
        //2.3封面类型为自动
        if(dto.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)) {
            wmNews.setType(null);
        }
        saveOrUpdateWmNews(wmNews);
        //3.判断是否为草稿
        if(dto.getStatus().equals(WmNews.Status.NORMAL.getCode())) {
            //直接保存结束
            return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
        }
        //4.不是草稿
        //4.1保存文章图片素材与文章关系
        //4.1.1提取图片素材列表
        List<String> imagesList = getImagesList(dto);
        //4.1.2保存
        saveRelatedImages(imagesList,wmNews.getId(),WemediaConstants.WM_CONTENT_REFERENCE);
        //4.2保存封面图片和文章关系
        saveRelatedCover(dto,imagesList,wmNews);
        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }
    @Autowired
    private WmNewsMaterialMapper wmNewsMaterialMapper;
    private void saveOrUpdateWmNews(WmNews wmNews) {
        wmNews.setUserId(WmThreadLocalUtils.getUser().getId());
        wmNews.setCreatedTime(new Date());
        wmNews.setSubmitedTime(new Date());
        wmNews.setEnable((short) 1);
        if(wmNews.getId() == null) {
            //保存
            save(wmNews);
        } else {
            //修改
            //删除文章和素材的关系
            wmNewsMaterialMapper.delete(Wrappers.<WmNewsMaterial>lambdaQuery().eq(WmNewsMaterial::getNewsId,wmNews.getId()));
            updateById(wmNews);
        }
    }
    /**
     * 获取文章图片素材列表
     * @param dto
     * @return
     */
    private List<String> getImagesList(WmNewsDto dto) {
        List<String> imagesUrlList = new ArrayList<>();
        String content = dto.getContent();
        List<Map> maps = JSON.parseArray(content, Map.class);
        for(Map map : maps) {
            if(map.get("type").equals("image")) {
                String imageUrl = (String) map.get("value");
                imagesUrlList.add(imageUrl);
            }
        }
        return imagesUrlList;
    }
    @Autowired
    private WmMaterialMapper wmMaterialMapper;
    /**
     * 保存图片素材与文章的关系
     * @param imagesList
     * @param id
     */
    private void saveRelatedImages(List<String> imagesList, Integer id,Short type) {
        //参数校验
        if(imagesList != null && !imagesList.isEmpty()) {
            //通过图片url获取素材id
            List<WmMaterial> materials = wmMaterialMapper.selectList(Wrappers.<WmMaterial>lambdaQuery().in(WmMaterial::getUrl, imagesList));
            //判断素材是否有效
            if(materials == null || materials.isEmpty()) {
                //手动抛出异常 一方面提醒开发者,另一方面做数据回滚
                throw new CustomException(AppHttpCodeEnum.MATERIASL_REFERENCE_FAIL);
            }
            //素材部分失效
            if(materials.size() != imagesList.size()) {
                throw new CustomException(AppHttpCodeEnum.MATERIASL_REFERENCE_FAIL);
            }
            //获取素材id
            List<Integer> materialsId = materials.stream().map(WmMaterial::getId).collect(Collectors.toList());
            //批量保存
            wmNewsMaterialMapper.saveRelations(materialsId,id,type);
        }
    }
    /**
     * 保存封面图片与文章之间关系
     * @param dto
     * @param imagesList
     * @param wmNews
     */
    private void saveRelatedCover(WmNewsDto dto, List<String> imagesList, WmNews wmNews) {
        List<String> images = dto.getImages();
        //自动设置封面
        if(dto.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)) {
            //多图
            if(imagesList.size() >= 3) {
                //设置文章封面属性
                wmNews.setType(WemediaConstants.WM_NEWS_MANY_IMAGE);
                images = imagesList.stream().limit(3).collect(Collectors.toList());
            }
            //单图
            else if(imagesList.size() >= 1) {
                //设置文章封面属性
                wmNews.setType(WemediaConstants.WM_NEWS_SINGLE_IMAGE);
                images = imagesList.stream().limit(1).collect(Collectors.toList());
            }
            //无图
            else {
                //设置文章封面属性
                wmNews.setType(WemediaConstants.WM_NEWS_NONE_IMAGE);
            }
            //修改文章封面信息
            if(images != null && images.size() != 0) {
                wmNews.setImages(StringUtils.join(images,","));
            }
            updateById(wmNews);
        }
        if(images != null && images.size() != 0) {
            saveRelatedImages(images,wmNews.getId(),WemediaConstants.WM_COVER_REFERENCE);
        }
    }
}

image.gif

5.代码说明

前端传过来的数据格式如下:

{
    "title":"",
    "type":"1",//这个 0 是无图  1 是单图  3 是多图  -1 是自动
    "labels":"",
    "publishTime":"2022-03-14T11:35:49.000Z",
    "channelId":1,
    "images":[
        "http://192.10/group1/M00/00/00/wKjIgl5swbGATaSAAAEPfZfx6Iw790.png"
    ],
    "status":1,
    "content":"[
    {
        "type":"text",
        "value":"随着智能手机的普及,人们更加习惯于通过手机来看新闻。"
    },
    {
        "type":"image",
        "value":"http://19.130/group1/M00/00/00/wKjIgl790.png"
    }
]"
}

image.gif

       这是JSON格式的字符串,里面的images表示文章的封面信息,是一个数组类型,但是自媒体文章实体类WmNews中的封面属性iamges是一个字符串类型,若有多个封面则用","隔开,所以在保存封面之前需要对前端传过来的数据进行处理。需要注意的是,content包含两个部分,一个是文本内容,一个是图片内容。因此在获取文章图片素材列表时候我们使用的是Map来接收,并且key值为"image"。

下篇预告:自媒体文章自动审核

相关文章
|
19天前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo:微服务通信的高效解决方案
【10月更文挑战第15天】随着信息技术的发展,微服务架构成为企业应用开发的主流。Spring Cloud Dubbo结合了Dubbo的高性能RPC和Spring Cloud的生态系统,提供高效、稳定的微服务通信解决方案。它支持多种通信协议,具备服务注册与发现、负载均衡及容错机制,简化了服务调用的复杂性,使开发者能更专注于业务逻辑的实现。
44 2
|
22天前
|
设计模式 前端开发 Java
Spring MVC——项目创建和建立请求连接
MVC是一种软件架构设计模式,将应用分为模型、视图和控制器三部分。Spring MVC是基于MVC模式的Web框架,通过`@RequestMapping`等注解实现URL路由映射,支持GET和POST请求,并可传递参数。创建Spring MVC项目与Spring Boot类似,使用`@RestController`注解标记控制器类。
30 1
Spring MVC——项目创建和建立请求连接
|
22天前
|
Java 关系型数据库 MySQL
Maven——创建 Spring Boot项目
Maven 是一个项目管理工具,通过配置 `pom.xml` 文件自动获取所需的 jar 包,简化了项目的构建和管理过程。其核心功能包括项目构建和依赖管理,支持创建、编译、测试、打包和发布项目。Maven 仓库分为本地仓库和远程仓库,远程仓库包括中央仓库、私服和其他公共库。此外,文档还介绍了如何创建第一个 SpringBoot 项目并实现简单的 HTTP 请求响应。
99 1
Maven——创建 Spring Boot项目
|
25天前
|
Java 关系型数据库 MySQL
如何使用 maven 创建一个 Spring Boot项目
Maven 是一个强大的项目管理工具,通过配置 `pom.xml` 文件自动获取所需的 jar 包,提高开发效率。其核心功能包括项目构建和依赖管理。项目构建支持编译、测试、打包和发布等流程,而依赖管理则通过中央仓库、本地仓库和私有服务器获取和管理项目依赖。示例中展示了如何创建第一个 SpringBoot 项目并实现简单接口。
21 1
如何使用 maven 创建一个 Spring Boot项目
|
22天前
|
Dubbo Java 应用服务中间件
Dubbo学习圣经:从入门到精通 Dubbo3.0 + SpringCloud Alibaba 微服务基础框架
尼恩团队的15大技术圣经,旨在帮助开发者系统化、体系化地掌握核心技术,提升技术实力,从而在面试和工作中脱颖而出。本文介绍了如何使用Dubbo3.0与Spring Cloud Gateway进行整合,解决传统Dubbo架构缺乏HTTP入口的问题,实现高性能的微服务网关。
|
18天前
|
JSON Java 数据格式
【微服务】SpringCloud之Feign远程调用
本文介绍了使用Feign作为HTTP客户端替代RestTemplate进行远程调用的优势及具体使用方法。Feign通过声明式接口简化了HTTP请求的发送,提高了代码的可读性和维护性。文章详细描述了Feign的搭建步骤,包括引入依赖、添加注解、编写FeignClient接口和调用代码,并提供了自定义配置的示例,如修改日志级别等。
40 1
|
22天前
|
人工智能 文字识别 Java
SpringCloud+Python 混合微服务,如何打造AI分布式业务应用的技术底层?
尼恩,一位拥有20年架构经验的老架构师,通过其深厚的架构功力,成功指导了一位9年经验的网易工程师转型为大模型架构师,薪资逆涨50%,年薪近80W。尼恩的指导不仅帮助这位工程师在一年内成为大模型架构师,还让他管理起了10人团队,产品成功应用于多家大中型企业。尼恩因此决定编写《LLM大模型学习圣经》系列,帮助更多人掌握大模型架构,实现职业跃迁。该系列包括《从0到1吃透Transformer技术底座》、《从0到1精通RAG架构》等,旨在系统化、体系化地讲解大模型技术,助力读者实现“offer直提”。此外,尼恩还分享了多个技术圣经,如《NIO圣经》、《Docker圣经》等,帮助读者深入理解核心技术。
SpringCloud+Python 混合微服务,如何打造AI分布式业务应用的技术底层?
|
22天前
|
Java Apache Maven
Java/Spring项目的包开头为什么是com?
本文介绍了 Maven 项目的初始结构,并详细解释了 Java 包命名惯例中的域名反转规则。通过域名反转(如 `com.example`),可以确保包名的唯一性,避免命名冲突,提高代码的可读性和逻辑分层。文章还讨论了域名反转的好处,包括避免命名冲突、全球唯一性、提高代码可读性和逻辑分层。最后,作者提出了一个关于包名的问题,引发读者思考。
Java/Spring项目的包开头为什么是com?
|
消息中间件 设计模式 缓存
Spring相关文章汇总篇【Spring,SpringBoot,SpringCloud等】
因为Spring框架包含的组件比较多,写的博客内容也比较多,虽然有分专栏但是依然不方便查找,所以专门用一篇文章来记录相关文章,会不定期更新。
Spring相关文章汇总篇【Spring,SpringBoot,SpringCloud等】
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。