非技术探讨:文章定时发布功能如何实现

Published on 2021-09-03 16:51 in 分类: 博客 with 狂盗一枝梅
分类: 博客

一、需求背景和方法论

最近接了产品一个需求,要做文章的定时发布和定时失效功能,除此之外要能方便的直接对文章进行隐藏操作,隐藏之后的文章就像过了有效期一样不再显示在前端。仔细思考了下这个需求,文章的状态应该有四种:草稿、未发布、已发布、已失效,草稿就不用说了,另外三种状态的变更是个问题,仔细想了想,有两种想法解决这个问题,也是很多人都会想到的方法:

  1. 定时跑批,根据有效期更改文章状态,查询的时候直接根据状态查询即可,这种方案的优点是查询简单,缺点是更改之后不能立即生效,必须跑批完成之后才会生效,这可能会导致错误的状态显示。
  2. 不做跑批,在查询的时候根据当前时间和有效期动态的计算文章状态并查询,这种方案的优点是数据库不保存文章的已发布、已失效、未发布状态,而是根据有效期实时计算的,所以实时性高,用户啥时候查啥时候计算,修改完有效期,文章状态会立即发生更改;这种方案的缺点是查询复杂,因为有效期要参与计算。

很明显,两相比较,第二种方法更加靠谱,虽然查询复杂了些,但是不需要每一种状态到数据库,维护的成本就要低很多,再加上有效期实时更改,我确认使用这种方式更加合理。

二、重新设计方法模型

1.模型重新梳理

在上面已经说过了,文章有四种状态:草稿、未发布、已发布、已失效,因为有效期要参与计算,动态的计算文章的状态,所以这时候文章维护在数据库中的状态要重新定义,这里直接简化为两种状态:

  1. 未发布,也就是原来的草稿状态
  2. 已发布,这里的已发布包含三种子状态,已发布(在有效期)已发布(未到有效期)已发布(过了有效期)

上述所有状态中,只有已发布(在有效期)的状态才能显示在前端。

结合显示和隐藏的需求,我用思维导图表示下

image-20210903155051003

2.发挥作用的优先级

第一优先级:显示状态,显示状态为显示,则判定第二优先级的内容;如果为隐藏,则文章直接不再显示

第二优先级:发布状态,发布状态为未发布状态,则文章不可以显示;如果是已发布状态,则需要判定第三优先级的内容

第三优先级:有效时间,在有效期内的,为真正的发布状态,可以显示;当前时间小于有效期的,则是已发布但是未到有效期的文章,不可以显示;当前时间大于有效期的,则为已发布但是过了有效期的文章,也就是已下架的文章,同样不可以显示。

用思维导图表示下优先级如下

image-20210903155659210

3.状态间的影响传递

产品一开始的需求是点击隐藏按钮之后把有效期时间的额开始时间和结束时间改下,具体如下:

文章为显示状态,点击隐藏按钮,点击按钮之后有效期开始时间不变,结束时间变成当前时间;文章为隐藏状态,点击显示按钮,点击按钮之后有效期开始时间变成当前时间,结束时间变成空,表示永久有效。

乍一听好像有些道理,实际上则是产品没理解上面图中状态的影响传递。

根据上一小节的优先级判定顺序,判定顺序是 $$ 显示状态->发布状态->有效时间 $$ 那影响顺序则是导过来的 $$ 有效时间->发布状态->显示状态 $$ 所以说不应该是显示状态的修改影响有效时间,应该有效时间的更改影响显示状态(实际在做的时候不用去修改显示状态,几个状态相互独立修改即可)

思维导图如下

image-20210903160857191

三、实现

1.表设计

只要想好了上述方法论的细节,则不难设计表结构

CREATE TABLE `article` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `TITLE` varchar(100) DEFAULT NULL COMMENT '咨询标题',
  `SECOND_TITLE` varchar(300) DEFAULT NULL COMMENT '副标题',
  `CONTENT` mediumtext COMMENT '内容',
  `FLAG` tinyint(4) DEFAULT NULL COMMENT '发布状态,0:未发布,1:已发布',
  `VALIDITY_START_TIME` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '有效期开始时间,默认为当前时间',
  `VALIDITY_END_TIME` datetime DEFAULT NULL COMMENT '有效期结束时间,为空表示永久有效',
  `AUTHOR` varchar(255) DEFAULT NULL COMMENT '作者(非创建人,页面填写的)',
  `PUBLISH_TIME` datetime DEFAULT NULL COMMENT '发布时间',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章存储表';

其中有三个字段是和文章状态和有效期相关的

`FLAG` tinyint(4) DEFAULT NULL COMMENT '发布状态,0:未发布,1:已发布',
`VALIDITY_START_TIME` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '有效期开始时间,默认为当前时间',
`VALIDITY_END_TIME` datetime DEFAULT NULL COMMENT '有效期结束时间,为空表示永久有效',

设计上,数据库只维护两种文章状态:未发布,也就是草稿状态;已发布,里面包含着未到有效期、在有效期、过了有效期的三种子状态,这三种子状态需要根据有效期动态的计算。

2.代码实现:运营端查询

image-20210903162411709

对应的参数是

  • flag:0:未发布;1:已发布;2:已发布(未到发布时间);3:已下架

第一步:状态转换

前端传过来四种状态,数据库只存了两种,要把四种状态转换成两种状态

image-20210903163053374

对应的java代码如下

String realFlag = flag;
if (!StringUtils.isEmpty(flag)) {
    realFlag = (flag.equalsIgnoreCase("2")
                || flag.equalsIgnoreCase("3"))
        ? "1" : flag;
}

第二步:有效期筛选

这里使用mybatis plus的代码演示

筛选数据库中和对应realFlag一致的文章

lambdaQueryWrapper.eq(!StringUtils.isEmpty(flag), ArticleEntity::getFlag, realFlag);

筛选已经发布且在有效期内或者永久有效的内容

 if (!StringUtils.isEmpty(flag) && flag.equalsIgnoreCase(1)) {
            LocalDateTime now = LocalDateTime.now();
            lambdaQueryWrapper.and(wrapper -> {
                wrapper.and(i -> {
                    i.le(ArticleEntity::getValidityStartTime, now.format(DateTimeFormatter.ofPattern(DateTimeFormat.DEFAULT_FORMAT)));
                    i.ge(ArticleEntity::getValidityEndTime, now.format(DateTimeFormatter.ofPattern(DateTimeFormat.DEFAULT_FORMAT)));
                });
                wrapper.or();
                wrapper.isNull(ArticleEntity::getValidityEndTime);
            });
        }

筛选 已发布,但是未到发布时间(当前时间小于有效期时间)

if (!StringUtils.isEmpty(flag) && flag.equalsIgnoreCase("2")) {
            LocalDateTime now = LocalDateTime.now();
            lambdaQueryWrapper.and(wrapper -> {
                wrapper.gt(ArticleEntity::getValidityStartTime, now.format(DateTimeFormatter.ofPattern(DateTimeFormat.DEFAULT_FORMAT)));
            });
        }

筛选 已下架(当前时间大于有效期时间)的状态 的记录

if (!StringUtils.isEmpty(flag) && flag.equalsIgnoreCase("3")) {
            LocalDateTime now = LocalDateTime.now();
            lambdaQueryWrapper.and(wrapper -> {
                wrapper.lt(ArticleEntity::getValidityEndTime, now.format(DateTimeFormatter.ofPattern(DateTimeFormat.DEFAULT_FORMAT)));
            });
        }

3.查询效果演示

效果演示

以上是我瞎琢磨的,如果有更好的设计方案,欢迎在评论区留言讨论~


#定时发布
目录