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

8420b26844034fab91b6df661ae68671.png

个人简介: 

> 个人主页:赵四司机
> 学习方向:JAVA后端开发 
> 种一棵树最好的时间是十年前,其次是现在!
> ⏰往期文章:SpringBoot项目整合微信支付
> 喜欢的话麻烦点点关注喔,你们的支持是我的最大动力。

前言:

最近在做一个基于SpringCloud+Springboot+Docker的新闻头条微服务项目,现在项目开发进入了尾声,我打算通过写文章的形式进行梳理一遍,并且会将梳理过程中发现的Bug进行修复,有需要改进的地方我也会继续做出改进。这一系列的文章我将会放入微服务项目专栏中,这个项目适合刚接触微服务的人作为练手项目,假如你对这个项目感兴趣你可以订阅我的专栏进行查看,需要资料可以私信我,当然要是能给我点个小小的关注就更好了,你们的支持是我最大的动力。

目录

一:获取所有频道

1.需求分析

2.表结构

3.接口定义

4.功能实现

二:查询文章

1.需求说明

2.表结构

3.接口定义

4.功能实现

三:文章发布

1.需求分析

2.表结构

3.实现思路

4.代码实现

5.代码说明


一:获取所有频道

1.需求分析

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

【Spring Cloud】新闻头条微服务项目:自媒体文章管理_第1张图片

2.表结构

【Spring Cloud】新闻头条微服务项目:自媒体文章管理_第2张图片数据库表字段有频道名称、频道描述、是否默认频道、频道状态、默认排序、创建时间,其对应的实体类为:

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;

/**
 * 

* 频道信息表 *

* * @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; }

3.接口定义

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

4.功能实现

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

二:查询文章

1.需求说明

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

2.表结构

【Spring Cloud】新闻头条微服务项目:自媒体文章管理_第3张图片        自媒体文章表字段比较多,主要包括用户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;

/**
 * 

* 自媒体图文内容信息表 *

* * @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; } } }

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;
}

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 implements WmNewsService {
    /**
     * 查找文章内容
     * @param dto
     * @return
     */
    @Override
    public ResponseResult findContentList(WmNewsPageReqDto dto) {
        //1.参数检查
        dto.checkParam();

        //2.分页条件查询
        IPage page = new Page<>(dto.getPage(),dto.getSize());
        LambdaQueryWrapper 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;
    }
}

三:文章发布

1.需求分析

【Spring Cloud】新闻头条微服务项目:自媒体文章管理_第4张图片

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

2.表结构

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

wm_material 素材表

wm_news_material 文章素材关系表  

这三张表的关系见下图:

【Spring Cloud】新闻头条微服务项目:自媒体文章管理_第5张图片可以看到文章表、素材表和素材关系表之间的关系都是一对多的关系,因为一篇文章可能包含多张素材,一张素材也可能被多次引用。

3.实现思路

【Spring Cloud】新闻头条微服务项目:自媒体文章管理_第6张图片  

        当创作者点击保存草稿或者提交审核之后,首先应根据文章有无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 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 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.lambdaQuery().eq(WmNewsMaterial::getNewsId,wmNews.getId()));
            updateById(wmNews);
        }
    }

    /**
     * 获取文章图片素材列表
     * @param dto
     * @return
     */
    private List getImagesList(WmNewsDto dto) {
        List imagesUrlList = new ArrayList<>();
        String content = dto.getContent();
        List 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 imagesList, Integer id,Short type) {
        //参数校验
        if(imagesList != null && !imagesList.isEmpty()) {
            //通过图片url获取素材id
            List materials = wmMaterialMapper.selectList(Wrappers.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 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 imagesList, WmNews wmNews) {
        List 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);
        }
    }
}

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"
    }
]"
}

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

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

你可能感兴趣的:(#,微服务项目,java,spring,cloud,微服务,后端,spring,boot)