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

简介: 介绍了自媒体创作端文章内容的展示以及文章的发布。

一:获取所有频道

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"。

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

相关文章
|
7月前
|
应用服务中间件 Nacos nginx
黑马头条_SpringCloud项目阶段一:环境搭建(Mac版本)
本文为 Mac 用户介绍微服务项目环境搭建,含阿里云服务器用 Docker 装 Nacos 1.2.0,本地通过 brew 装 OpenJDK 8、Maven 3.6.1、Redis,Docker 部署 MySQL 5.7 并配字符集,及 Nginx 安装与反向代理设置,附命令与配置步骤。
411 4
黑马头条_SpringCloud项目阶段一:环境搭建(Mac版本)
|
9月前
|
Java 关系型数据库 数据库连接
Spring Boot项目集成MyBatis Plus操作PostgreSQL全解析
集成 Spring Boot、PostgreSQL 和 MyBatis Plus 的步骤与 MyBatis 类似,只不过在 MyBatis Plus 中提供了更多的便利功能,如自动生成 SQL、分页查询、Wrapper 查询等。
878 4
|
9月前
|
前端开发 Java API
酒店管理系统基于 JavaFX Spring Boot 和 React 经典项目重构实操
本文介绍了基于现代技术栈的酒店管理系统开发方案,整合了JavaFX、Spring Boot和React三大技术框架。系统采用前后端分离架构,JavaFX构建桌面客户端,React开发Web管理界面,Spring Boot提供RESTful API后端服务。核心功能模块包括客房管理和客户预订流程,文中提供了JavaFX实现的客房管理界面代码示例和React开发的预订组件代码,展示了如何实现客房信息展示、添加修改操作以及在线预订功能。
579 0
|
9月前
|
Java 测试技术 Spring
简单学Spring Boot | 博客项目的测试
本内容介绍了基于Spring Boot的博客项目测试实践,重点在于通过测试驱动开发(TDD)优化服务层代码,提升代码质量和功能可靠性。案例详细展示了如何为PostService类编写测试用例、运行测试并根据反馈优化功能代码,包括两次优化过程。通过TDD流程,确保每项功能经过严格验证,增强代码可维护性与系统稳定性。
336 0
|
9月前
|
存储 Java 数据库连接
简单学Spring Boot | 博客项目的三层架构重构
本案例通过采用三层架构(数据访问层、业务逻辑层、表现层)重构项目,解决了集中式开发导致的代码臃肿问题。各层职责清晰,结合依赖注入实现解耦,提升了系统的可维护性、可测试性和可扩展性,为后续接入真实数据库奠定基础。
682 0
|
9月前
|
Java 应用服务中间件 Maven
第01课:Spring Boot开发环境搭建和项目启动
第01课:Spring Boot开发环境搭建和项目启动
2471 0
|
12月前
|
SQL 前端开发 Java
深入理解 Spring Boot 项目中的分页与排序功能
本文深入讲解了在Spring Boot项目中实现分页与排序功能的完整流程。通过实际案例,从Service层接口设计到Mapper层SQL动态生成,再到Controller层参数传递及前端页面交互,逐一剖析每个环节的核心逻辑与实现细节。重点包括分页计算、排序参数校验、动态SQL处理以及前后端联动,确保数据展示高效且安全。适合希望掌握分页排序实现原理的开发者参考学习。
759 4
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
798 6
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
413 1