个人简介:
> 📦个人主页:赵四司机
> 🏆学习方向:JAVA后端开发
> 📣种一棵树最好的时间是十年前,其次是现在!
> ⏰往期文章:SpringBoot项目整合微信支付
> 🧡喜欢的话麻烦点点关注喔,你们的支持是我的最大动力。
前言:
最近在做一个基于SpringCloud+Springboot+Docker的新闻头条微服务项目,现在项目开发进入了尾声,我打算通过写文章的形式进行梳理一遍,并且会将梳理过程中发现的Bug进行修复,有需要改进的地方我也会继续做出改进。这一系列的文章我将会放入微服务项目专栏中,这个项目适合刚接触微服务的人作为练手项目,假如你对这个项目感兴趣你可以订阅我的专栏进行查看,需要资料可以私信我,当然要是能给我点个小小的关注就更好了,你们的支持是我最大的动力。
目录
一:获取所有频道
1.需求分析
2.表结构
3.接口定义
4.功能实现
二:查询文章
1.需求说明
2.表结构
3.接口定义
4.功能实现
三:文章发布
1.需求分析
2.表结构
3.实现思路
4.代码实现
5.代码说明
一:获取所有频道
1.需求分析
当我们点击内容管理时候,页面会自动发送请求获取频道列表(Java、MySql、大数据、推荐等),这时候用户可以进行频道的选择以过滤其他频道的文章。
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;
/**
* <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;
}
3.接口定义
说明 | |
---|---|
接口路径 | /api/v1/channel/channels |
请求方式 | POST |
参数 | 无 |
响应结果 | ResponseResult |
4.功能实现
实现代码不难,就是简单地从数据库中获取所有频道的信息并返回,为了节省篇幅我这里就不将代码放上来了,可以自己动手实现一下。
二:查询文章
1.需求说明
在内容列表页面,我们可以通过特定条件筛选文章,比如按照文章的状态、频道、发布时间等筛选出自己想要的文章信息。
2.表结构
自媒体文章表字段比较多,主要包括用户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;
}
}
}
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<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;
}
}
三:文章发布
1.需求分析
文章的发布是这个项目的难点之一,因为涉及到文章内容和素材的关系,这种关系又分为内容引用和封面引用两种。当用户选择的是自动设置封面时候,我们需要根据情况选择是设置无封面、单图封面、双图封面、多图封面。在提交部分,创作者可以选择保存为草稿,也可以选择提交审核,审核通过即可发表,此外,创作者还可以选择定时发布文章,不过审核部分和定时发布部分留到后面再说。
2.表结构
除了文章表之外,我们还需要另外两张表,即素材表和素材关系表:
wm_material 素材表
wm_news_material 文章素材关系表
这三张表的关系见下图:
可以看到文章表、素材表和素材关系表之间的关系都是一对多的关系,因为一篇文章可能包含多张素材,一张素材也可能被多次引用。
3.实现思路
当创作者点击保存草稿或者提交审核之后,首先应根据文章有无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);
}
}
}
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"。
下篇预告:自媒体文章自动审核