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

本文涉及的产品
服务治理 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"。

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

相关文章
|
4天前
|
Java 应用服务中间件 Maven
Spring Boot项目打war包(idea:多种方式)
Spring Boot项目打war包(idea:多种方式)
16 1
|
4天前
|
负载均衡 监控 算法
【微服务 SpringCloud】实用篇 · Eureka注册中心
【微服务 SpringCloud】实用篇 · Eureka注册中心
12 1
|
4天前
|
存储 SpringCloudAlibaba Java
【微服务 SpringCloud】实用篇 · 服务拆分和远程调用
【微服务 SpringCloud】实用篇 · 服务拆分和远程调用
19 2
|
2天前
|
前端开发 JavaScript Java
Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)五(前端页面
Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)五(前端页面
Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)五(前端页面
|
3天前
|
消息中间件 Java 数据安全/隐私保护
Spring Cloud 项目中实现推送消息到 RabbitMQ 消息中间件
Spring Cloud 项目中实现推送消息到 RabbitMQ 消息中间件
|
4天前
|
Prometheus 监控 负载均衡
【SpringCloud】微服务重点解析
【SpringCloud】微服务重点解析
13 0
|
4天前
|
缓存 负载均衡 算法
【微服务 SpringCloud】实用篇 · Ribbon负载均衡
【微服务 SpringCloud】实用篇 · Ribbon负载均衡
13 0
|
4天前
|
Java 应用服务中间件 Spring
【JavaEE】Spring Boot - 项目的创建和使用(下)
【JavaEE】Spring Boot - 项目的创建和使用
8 0
|
4天前
|
数据可视化 Java 应用服务中间件
【JavaEE】Spring Boot - 项目的创建和使用(上)
【JavaEE】Spring Boot - 项目的创建和使用
9 0
|
4天前
|
存储 Java 对象存储
【JavaEE】DI与DL的介绍-Spring项目的创建-Bean对象的存储与获取
【JavaEE】DI与DL的介绍-Spring项目的创建-Bean对象的存储与获取
9 0