分享一個網上搜不到的「Redis」實現「聊天回合制」的方案

語言: CN / TW / HK

theme: vuepress highlight: vs2015


前言

為什麼說網上搜不到,因為關於聊天回合制的方案作者本人快把百度搜禿嚕了也沒找到,好在最終是公司一個關係不錯的大佬幫提供了點思路,最終作者將其完整實現了出來。

分享出來大家可以收藏,萬一你哪天也碰到這樣的需求,可不就節省大把時間了嗎。


場景

先說下我這邊的場景,讀過我文章的同好都知道,我是做網際網路醫療行業的,我們的專案中是包含聊天功能的,我們服務的物件主要是醫院的醫生,患者在網上找醫生問診時,往往會出現不停問的情況。

醫生目前唯一的做法是自己結束這個諮詢,或等待系統自動結束,這就帶來了一個問題,不管是系統結束還是醫生手動結束,患者都喜歡投訴和打差評,導致醫生不敢擅自結束,問煩了又不好不回覆,不回覆也要被投訴。

最終聊天回合制這個需求就擺出來了,主動告訴患者我們的聊天是有回合的,所以你要一次問清楚,回合數滿了我們不會再回復,如果患者硬要投訴,醫生也可以說,這是做這個產品的公司自己設定的。

結下來就是,我們要把鍋端好。

實際上,聊天回合制的誕生,基本上都和這個場景的訴求類似,為了減少使用者頻繁且無休止的諮詢。


思路

結合redis能夠很好的實現聊天回合制,當然也可以直接通過資料庫來實現,但顯然redis操作更簡單效能更優越。

總體思路如下:

1)、redis中儲存兩個key,一個是表示物件,宣告為chat-who:consultId,value為物件標識,比如這裡就是醫生和患者,醫生用D標識,患者用P標識;另一個key是表示回合數,宣告為chat-num:consultId,value就是當前回合數。這裡的consultId是動態的,表示這個諮詢的id,可以根據自己的業務來定;

2)、這兩個key的過期時間我們都定為2天,具體過期時間要根據自己業務規則來適配;

3)、我們在特定的位置進行初始化,只要是進入聊天之前都可以,比如這裡的場景,就是患者發起諮詢成功後才開始聊天,我們就在發起成功後的方法中初始化聊天回合數為預設值6個回合,這個預設值還可以做成配置的形式進行動態讀取的;

4)、我們在發訊息的方法中做一個判斷,獲取redis中的chat-who:consultId,看是否存在,存在就往下執行,不存在就說明發的是第一條訊息,那就建立chat-who:consultId這個key到redis中,value為當前發訊息人D或者P;

5)、承接4,如果chat-who存在,我們繼續將當前發訊息的物件和redis的chat-who儲存的物件值進行比較,如果一樣,則跳過不管,如果不一樣,更新chat-who的值為當前發訊息的人。同時,我們判斷當前發訊息的人是不是醫生也就是D,是D的話才更新回合數,執行-1操作,這樣做的目的是把醫生作為回合數更新的維度,維度只能有一個,這樣才能保證回合數更新最準確。


實現

接下來,我使用虛擬碼把整個思路寫出來。

1、定義redis-key

java /**  * 聊天回合制常量  */ public final class ChatRoundConstants { /**      * 聊天回合數key字首      */     public static final String CHAT_NUM = "chat-num:";     /**      * 聊天物件key字首      */     public static final String CHAT_WHO = "chat-who:";     /**      * redis-key過期時間      */     public static final Long EXPIRE_TIME = 48 * 3600L;     /**      * 聊天物件value值,醫生-D,患者-P。      */     public static final String DOCTOR = "D";     public static final String PATIENT = "P"; }

2、初始化聊天回合數

在聊天之前初始化,這裡我們專案的場景是患者發起諮詢成功後,就在這個成功後的方法中初始化。

```java /* * 發起諮詢成功 / public void consultSuccess() { // ....其他業務邏輯處理

// 初始化聊天回合數
initChatRoundNum(ConsultDTO consultDTO);

}

/* * 初始化聊天回合數 * -- 過期時間48小時 * @param consultDTO 諮詢資訊 / private void initChatRoundNum(ConsultDTO consultDTO) { // 初始6回合 int chatNum = 6;

// 獲取系統配置的預設回合數,這裡是虛擬碼根據自己需要編寫。
ParameterDTO parameterDTO = getConfigValue();
if(!ObjectUtils.isEmpty(parameterDTO)) {
    chatNum = parameterDTO.getPvalue();
}

// 初始化到redis,key是chat-num:consultId
redisService.set(ChatRoundConstants.CHAT_NUM + consultDTO.getId(), 
    chatNum, ChatRoundConstants.EXPIRE_TIME);

} ```

3、更新回合數

這裡是核心邏輯,主要分為兩步:初始化chat-who:consultId,更新chat-num:consultId。

```java /* * 發訊息 / public void sendMsg() { // ....其他業務邏輯

// 更新聊天回合數
handleChatRoundNum(consultDTO, consultDetailInfoDTO);

}

/* * 處理聊天回合數 * @param consultDTO 諮詢資訊 * @param consultDetailInfoDTO 聊天資訊 / private void handleChatRoundNum(ConsultDTO consultDTO,  ConsultDetailInfoDTO consultDetailInfoDTO) {

// 獲取redis儲存的醫生患者標識key
String chatWhoKey = ChatRoundConstants.CHAT_WHO + consultDTO.getId();

// 獲取當前發訊息的人對應的標識
String current = ChatWhoEnum.getCodeById(consultDetailInfoDTO.getSource());

// chat-who:consultId是否存在
if(redisService.exists(chatWhoKey)) {

    String chatWhoValue = (String) redisService.get(chatWhoKey);

    // 判斷當前發訊息的人和chatWho的值是否相同,如果不同,更新chatWho為當前發訊息的人。
    if(!Objects.equals(ChatWhoEnum.getIdByCode(chatWhoValue), 
    consultDetailInfoDTO.getSource())) {

        // 更新chatWho為當前發訊息的人
        redisService.setRange(chatWhoKey, current, 0);

        // 判斷當前發訊息的人是否為D,是D的話才更新回合數。
        if(Objects.equals(ChatWhoEnum.DOCTOR.getId(), 
                            consultDetailInfoDTO.getSource())) {

            // 更新chatNum-1
            String chatNumKey = ChatRoundConstants.CHAT_NUM + consultDTO.getId();
            int chatNumValue = Integer.parseInt(
                                    (String) redisService.get(chatNumKey)
                                );
            if(redisService.exists(chatNumKey) && chatNumValue > 0) {
                redisService.decr(chatNumKey);
            }

        }

    }
} else {
    // 不存在說明是第一條訊息,建立這個key。
    redisService.set(chatWhoKey, current, ChatRoundConstants.EXPIRE_TIME);
}

} ```

定義的發訊息物件列舉

```java /* * 聊天物件來源的列舉類 / public enum ChatWhoEnum { // 來源 : // 0 醫生 // 1 患者 DOCTOR(0, "D", "醫生"), PATIENT(1, "P", "患者");

private final int id;
private final String code;
private final String label;

ChatWhoEnum(final int id, final String code, final String label) {
    this.id = id;
    this.code = code;
    this.label = label;
}

public int getId() {
    return id;
}
public String getCode() {
    return code;
}
public String getLabel() {
    return label;
}

public static String getCodeById(int id) {
    for(ChatWhoEnum type: ChatWhoEnum.values()) {
        if(type.getId() == id) {
            return type.getCode();
        }
    }
    return null;
}

public static Integer getIdByCode(String code) {
    for(ChatWhoEnum type: ChatWhoEnum.values()) {
        if(code.equalsIgnoreCase(type.getCode())) {
            return type.getId();
        }
    }
    return null;
}

} ```


總結

其實寫起來很簡單,思路也不難,但忽然間讓你來實現這個小功能的話還是挺費勁的,理不清楚就會一直卡在裡面,理清楚了瞬間就念頭通達。

這個功能目前已經上線,並且執行穩定沒有任何問題,感興趣的可以收藏起來,如果有一天做聊天相關業務的話,說不定就會遇到類似的需求。



本人原創文章純手打,覺得有一滴滴幫助就請點個贊和收藏吧~
本人持續分享實際工作經驗和主流技術,喜歡的話可以關注下哦~