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

福隆苑居士發表於2022-06-18

前言

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


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


場景

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


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


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


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


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


思路

結合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

/**
 * 聊天回合制常量
 */
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、初始化聊天回合數

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

/**
 * 發起諮詢成功
 */
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。

/**
 * 發訊息
 */
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);
    }
}

定義的發訊息物件列舉

/**
 * 聊天物件來源的列舉類
 */
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;
    }
}

總結

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


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



本人原創文章純手打,覺得有一滴滴幫助就請點個推薦吧~

本人持續分享實際工作經驗和主流技術,喜歡的話可以關注下哦~

相關文章