客服+機器人客服 架構思想

注:如果您發現任何不正確的內容,或者您想要分享有關本文主題的更多信息,請撰寫評論或聯繫我 [email protected]
如有侵權請聯繫我刪除。環信的API是公開的,應該沒啥事吧~~~


分享一次自實現IM的經驗。自己實現了一個簡略版本的IM。勉強可以用,就是頁面需要自己實現。雖然最終還是使用了環信的產品。因爲自己架構的不太成熟。沒有考慮到很多因素。果然啊自己造的就是沒有別人已經成熟的產品,使用的更堅挺、持久,圓潤~!

調用環信工具類

/**
 * 環信常量類
 *
 * @Date 2019/1/8-15:24
 * @Author Amarone
 * @Description you should see
 * <pre>
 * 1. http://api-docs.easemob.com/#/%E8%8E%B7%E5%8F%96token
 * 2. http://docs-im.easemob.com/im/100serverintegration/10intro
 * </pre>
 **/
public class EasemobConst {
    public static String SEGMENTATION = "/";

    //***************************************************     基礎屬性配置    *******************************************

    /**
     * IM 登錄統一默認密碼
     */
    public static String IM_DEFAULT_PASSWORD = "";

    /**
     * 企業的唯一標識 開發者在環信開發者管理後臺註冊賬號時填寫的企業 ID 此處從配置文件獲取
     */
    public static String BASE_ORG_NAME = ****;

    /**
     * 同一“企業”下“APP”唯一標識,開發者在環信開發者管理後臺創建應用時填寫的“應用名稱”
     */
    public static String BASE_APP_NAME = *****;

    /**
     * 請求環信接口TOKEN
     */
    public static String BASE_TOKEN = "";

    /**
     * 基礎屬性 - 環信後臺配置
     */
    public static String BASE_CLIENT_ID = ****;

    /**
     * 基礎屬性 - 環信後臺配置
     */
    public static String BASE_CLIENT_SECRET = ****;

    //***************************************************  API 接口URL 配置    ******************************************************

    /**
     * 環信公共請求URL、請求鏈接參考 http://api-docs.easemob.com/#!/%E7%94%A8%E6%88%B7%E4%BD%93%E7%B3%BB%E9%9B%86%E6%88%90/post_org_name_app_name_users
     * 格式爲: ${COMMON_PATH}/{org_name}/{app_name}/users
     */
    public static String API_BASE_PATH = "http://a1.easemob.com/" + BASE_ORG_NAME + SEGMENTATION + BASE_APP_NAME;

    /**
     * 獲取token
     * POST /{org_name}/{app_name}/token
     */
    public static String API_URL_GETTOKEN = "/token";

    /**
     * /{org_name}/{app_name}/users/{owner_username}/contacts/users/{friend_username}
     *
     * 向IM 用戶添加好友
     */
    public static String API_URL_ADD_FRIEND = "/users/{owner_username}/contacts/users/{friend_username}";

    /**
     * API 接口  用戶體系集成
     */
    public static String API_URL_USER_SYSTEM = "/users";

    /**
     * API 接口  查看一個用戶的在線狀態。 GET /{org_name}/{app_name}/users/{username}/status
     */
    public static String API_URL_ONLINE_STATUS = "/users/{username}/status";

    /**
     * 離線消息數量 GET /{org_name}/{app_name}/users/{owner_username}/offline_msg_count
     */
    public static String API_URL_OFFLINE_MSG = "/users/{owner_username}/offline_msg_count";


    /**
     * 解除好友關係 DELETE /{org_name}/{app_name}/users/{owner_username}/contacts/users/{friend_username}
     */
    public static String API_URL_RELIEVE_FRIEND = "/users/{owner_username}/contacts/users/{friend_username}";

}
/**
 * 獲取token
 *
 * @Date 2019/1/11-14:45
 * @Author Amarone
 * @Description
 **/
public class GetToken {

    private String grant_type = "client_credentials";
    
    // 從配置文件獲取
    private String client_id = ***;
     // 從配置文件獲取
    private String client_secret = ****;

    // 省略get set
}

/**
 * 註冊賬戶
 *
 * @Date 2019/1/15-10:57
 * @Author Amarone
 * @Description
 **/
public class RegisterAcc {

    /**
     * 用戶名
     */
    private String username;
    /**
     * 密碼
     */
    private String password;
    /**
     * 暱稱
     */
    private String nickname;

   // 省略get set 有參 無參 構造
}
/**
 * @Date 2019/1/12-18:04
 * @Author Amarone
 * @Description
 **/
public class RspToken {

    /**
     * token 值
     */
    private String access_token;
    /**
     * token 有效時間,以秒爲單位,在有效期內不需要重複獲取
     */
    private String expires_in;
    /**
     * 當前 APP 的 UUID 值
     */
    private String application;

   // 省略get set 有參 無參 構造
}
import org.apache.commons.codec.Charsets;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.LinkedHashMap;
import reactor.core.publisher.Mono;

/**
 * 環信工具類
 *
 * @Date 2019/1/8-15:23
 * @Author Amarone
 * @Description
 **/
@Service
public class EasemobUtil extends BaseLogger {

    /**
     * 發送post 請求
     *
     * @param method    請求的方法
     * @param url       請求的路徑
     * @param para      發送請求的參數
     * @param parameter 請求路徑中的參數
     * @return rsp
     */
    public LinkedHashMap prefixRequest(HttpMethod method, String url, Object para, Object... parameter) throws Exception {
        if (method == null) {
            throw new BizException("請求方法未識別!");
        }
        LinkedHashMap rsp = null;
        try {
            if (HttpMethod.POST.compareTo(method) == 0) {
                rsp = postRequest(url, para, parameter);
            } else if (HttpMethod.GET.compareTo(method) == 0) {
                rsp = getRequest(url, parameter);
            } else if (HttpMethod.DELETE.compareTo(method) == 0) {
                rsp = deleteRequest(url, parameter);
            } else {
                throw new BizException("請求方法未識別!");
            }
        } catch (Exception e) {
            if (e.getCause() instanceof EasemobException) {
                EasemobException ex = (EasemobException) e.getCause();
                if (String.valueOf(EasemobEnum.ANEW_TOKEN.getValue()).equals(ex.messageCode)) {
                    logger.warn("調用環信 401 異常,重新請求TOKEN");
                    // 401 錯誤爲 token過期
                    getEasemobToken();
                    // 重新發送請求
                    prefixRequest(method, url, para, parameter);
                } else if (String.valueOf(EasemobEnum.BAD_REQ.getValue()).equals(ex.messageCode)) {
                    logger.warn("調用環信 400 異常," + e.getMessage());
                } else {
                    e.printStackTrace();
                    throw ex;
                }
            } else {
                e.printStackTrace();
                throw e;
            }
        }
        return rsp;
    }


    /**
     * 刪除方法
     *
     * @param url       請求的路徑
     * @param parameter 請求路徑中的參數
     */
    public LinkedHashMap deleteRequest(String url, Object... parameter) throws Exception {
        if (StringUtils.isEmpty(EasemobConst.BASE_TOKEN)) {
            getEasemobToken();
        }
        WebClient webClient = buildBase();
        Mono<LinkedHashMap> result = webClient
                .delete()
                .uri(url, parameter)
                .header(HttpHeaders.AUTHORIZATION, EasemobConst.BASE_TOKEN)
                .acceptCharset(Charsets.UTF_8)
                .retrieve()
                .onStatus(e -> e.is4xxClientError(), resp -> {
                    return Mono.error(new EasemobException(String.valueOf(resp.statusCode().value()), resp.statusCode().getReasonPhrase()));
                })
                .onStatus(e -> e.is5xxServerError(), resp -> {
                    return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
                })
                .bodyToMono(LinkedHashMap.class)
                .doOnError(onerror -> {
                    logger.error("請求IM服務失敗,請稍後重試!");
                    onerror.printStackTrace();
                });
        return result.block();
    }

    /**
     * 發送post請求(本方法未處理異常)
     *
     * @param url       請求的路徑
     * @param para      發送請求的參數
     * @param parameter 請求路徑中的參數
     * @return rsp
     */
    public LinkedHashMap postRequest(String url, Object para, Object... parameter) throws Exception {
        if (StringUtils.isEmpty(EasemobConst.BASE_TOKEN)) {
            getEasemobToken();
        }
        WebClient webClient = buildBase();

        Mono<LinkedHashMap> result = null;
        if (parameter != null) {
            result = webClient
                    .post()
                    .uri(url, parameter)
                    .header(HttpHeaders.AUTHORIZATION, EasemobConst.BASE_TOKEN)
                    .acceptCharset(Charsets.UTF_8)
                    .syncBody(para)
                    .retrieve()
                    .onStatus(e -> e.is4xxClientError(), resp -> {
                        return Mono.error(new EasemobException(String.valueOf(resp.statusCode().value()), resp.statusCode().getReasonPhrase()));
                    })
                    .onStatus(e -> e.is5xxServerError(), resp -> {
                        return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
                    })
                    .bodyToMono(LinkedHashMap.class)
                    .doOnError(onerror -> {
                        logger.error("請求IM服務失敗,請稍後重試!");
                        onerror.printStackTrace();
                    });
        } else {
            result = webClient
                    .post()
                    .uri(url)
                    .header(HttpHeaders.AUTHORIZATION, EasemobConst.BASE_TOKEN)
                    .acceptCharset(Charsets.UTF_8)
                    .syncBody(para)
                    .retrieve()
                    .onStatus(e -> e.is4xxClientError(), resp -> {
                        return Mono.error(new EasemobException(String.valueOf(resp.statusCode().value()), resp.statusCode().getReasonPhrase()));
                    })
                    .onStatus(e -> e.is5xxServerError(), resp -> {
                        return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
                    })
                    .bodyToMono(LinkedHashMap.class);
        }

        if (result != null) {
            return result.block();
        } else {
            return null;
        }
    }

    /**
     * get 類型請求,應調用{prefixGetRequest()} 方法 (本方法未處理異常)
     *
     * @param url       請求url
     * @param parameter url中參數 使用 {}
     * @return result
     */
    public LinkedHashMap getRequest(String url, Object... parameter) throws Exception {
        if (StringUtils.isEmpty(EasemobConst.BASE_TOKEN)) {
            getEasemobToken();
        }
        WebClient webClient = buildBase();
        Mono<LinkedHashMap> result = webClient
                .get()
                .uri(url, parameter)
                .header(HttpHeaders.AUTHORIZATION, EasemobConst.BASE_TOKEN)
                .acceptCharset(Charsets.UTF_8)
                .retrieve()
                .onStatus(e -> e.is4xxClientError(), resp -> {
                    return Mono.error(new EasemobException(String.valueOf(resp.statusCode().value()), resp.statusCode().getReasonPhrase()));
                })
                .onStatus(e -> e.is5xxServerError(), resp -> {
                    return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
                })
                .bodyToMono(LinkedHashMap.class)
                .doOnError(onerror -> {
                    logger.error("請求IM服務失敗,請稍後重試!");
                    onerror.printStackTrace();
                });
        return result.block();
    }

    /**
     * 獲取環信token
     */
    void getEasemobToken() {
        WebClient webClient = buildBase();
        Mono<RspToken> rspMono = webClient
                .post()
                .uri(EasemobConst.API_URL_GETTOKEN)
                .syncBody(new GetToken())
                .retrieve()
                .onStatus(e -> e.is4xxClientError(), resp -> {
                    return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
                })
                .bodyToMono(RspToken.class);

        rspMono.doOnError(onerror -> {
            logger.error("請求IM服務失敗,請稍後重試!");
            onerror.printStackTrace();
        });

        RspToken rspToken = rspMono.block();
        // There are fucking Spaces
        EasemobConst.BASE_TOKEN = "Bearer " + rspToken.getAccess_token();
    }

    /**
     * 構建基礎請求
     */
    WebClient buildBase() {
        return WebClient
                .builder()
                .baseUrl(EasemobConst.API_BASE_PATH)
                .defaultHeaders(header -> {
                    header.setContentType(MediaType.APPLICATION_JSON);
                })
                .build();
    }
}

自實現IM

使用技術如下

  • - JAVA 1.8
  • - SpringBoot
  • - Websocket
  • - Netty
  • - RabbitMQ
  • - Redis

實現想法

總體流程:

  • - 客戶端指用戶使用的客戶端。客戶端統一使用Websocket發起連接請求到後臺的Netty服務器
  • - 客戶端連接成功之後,緊接着想服務器發起一段報文。報文內含 客戶端的用戶id等信息
  • - Netty接收到請求之後將Channel與客戶id綁定,存儲到java.util.map 裏面
  • - 每次接收到請求判斷map裏面是否存在消息接受者的id。如果沒有存到消息同步庫。待接收者上線後拉取信息

客服機器人:

  • - 客服機器人由後臺寫netty客戶端連接到服務器,機器人與普通客戶之間以客戶id開頭字母匹配。

以上寫的信息有點不詳細。想請教一下各位大神指出不足之處。

  • 暫不考慮傳輸 音頻、視頻等。目前只傳送純文本
  • 暫不考慮報文安全性
  • 暫不考慮報文大小、浪費的流量以及電量等
  • 暫不考慮大量併發
  • 暫不考慮分佈式部署

目前只是實現業務目標。

2018年12月20日

  • 現在又多了一個問題。怎麼把這玩意弄成高可用的、、

2018年12月21日

  • 如果管理channel等信息存儲在map裏面。這樣對於高可用的行不通的,一旦單點故障。整個IM就完蛋了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章