非技術探討:文章定時釋出功能如何實現

狂盜一枝梅發表於2021-09-03

一、需求背景和方法論

最近接了產品一個需求,要做文章的定時釋出和定時失效功能,除此之外要能方便的直接對文章進行隱藏操作,隱藏之後的文章就像過了有效期一樣不再顯示在前端。仔細思考了下這個需求,文章的狀態應該有四種:草稿、未釋出、已釋出、已失效,草稿就不用說了,另外三種狀態的變更是個問題,仔細想了想,有兩種想法解決這個問題,也是很多人都會想到的方法:

  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.查詢效果演示

效果演示

以上是我瞎琢磨的,如果有更好的設計方案,歡迎在評論區留言討論~

我的部落格原文:https://blog.kdyzm.cn/post/80

相關文章