最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot + WebSocket 實(shí)現(xiàn)答題對(duì)戰(zhàn)匹配機(jī)制案例詳解

 更新時(shí)間:2021年05月29日 09:54:17   作者:低吟不作語(yǔ)  
這篇文章主要介紹了SpringBoot + WebSocket 實(shí)現(xiàn)答題對(duì)戰(zhàn)匹配機(jī)制,分別為每個(gè)用戶擬定四種在線狀態(tài),通過(guò)流程圖給大家展示,需要的朋友可以參考下

概要設(shè)計(jì)

類(lèi)似競(jìng)技問(wèn)答游戲:用戶隨機(jī)匹配一名對(duì)手,雙方同時(shí)開(kāi)始答題,直到雙方都完成答題,對(duì)局結(jié)束?;镜倪壿嬀褪沁@樣,如果有其他需求,可以在其基礎(chǔ)上進(jìn)行擴(kuò)展

明確了這一點(diǎn),下面介紹開(kāi)發(fā)思路。為每個(gè)用戶擬定四種在線狀態(tài),分別是:待匹配、匹配中、游戲中、游戲結(jié)束。下面是流程圖,用戶的流程是被規(guī)則約束的,狀態(tài)也隨流程而變化

對(duì)流程再補(bǔ)充如下:

  • 用戶進(jìn)入匹配大廳(具體效果如何由客戶端體現(xiàn)),將用戶的狀態(tài)設(shè)置為待匹配
  • 用戶開(kāi)始匹配,將用戶的狀態(tài)設(shè)置為匹配中,系統(tǒng)搜索其他同樣處于匹配中的用戶,在這個(gè)過(guò)程中,用戶可以取消匹配,返回匹配大廳,此時(shí)用戶狀態(tài)重新設(shè)置為待匹配。匹配成功,保存匹配信息,將用戶狀態(tài)設(shè)置為游戲中
  • 根據(jù)已保存的匹配信息,用戶可以獲得對(duì)手的信息。答題是時(shí),每次用戶分?jǐn)?shù)更新,也會(huì)向?qū)κ滞扑透潞蟮姆謹(jǐn)?shù)
  • 用戶完成答題,則等待對(duì)手也完成答題。雙方都完成答題,用戶狀態(tài)設(shè)置為游戲結(jié)束,展示對(duì)局結(jié)果

詳細(xì)設(shè)計(jì)

針對(duì)概要設(shè)計(jì)提出的思路,我們需要思考以下幾個(gè)問(wèn)題:

  • 如何保持客戶端與服務(wù)器的連接?
  • 如何設(shè)計(jì)客戶端與服務(wù)端的消息交互?
  • 如何保存以及改變用戶狀態(tài)?
  • 如何匹配用戶?

下面我們一個(gè)一個(gè)來(lái)解決

1. 如何保持用戶與服務(wù)器的連接?

以往我們使用 Http 請(qǐng)求服務(wù)器,并獲取響應(yīng)信息。然而 Http 有個(gè)缺陷,就是通信只能由客戶端發(fā)起,無(wú)法做到服務(wù)端主動(dòng)向客戶端推送信息。根據(jù)概要設(shè)計(jì)我們知道,服務(wù)端需要向客戶端推送對(duì)手的實(shí)時(shí)分?jǐn)?shù),因此這里不適合使用 Http,而選擇了 WebSocket。WebSocket 最大的特點(diǎn)就是服務(wù)端可以主動(dòng)向客戶端推送信息,客戶端也可以主動(dòng)向服務(wù)端發(fā)送信息,是真正的雙向平等對(duì)話

有關(guān) SpringBoot 集成 WebSocket 可參考這篇博客:http://m.fzitv.net/article/208279.htm

2. 如何設(shè)計(jì)客戶端與服務(wù)端的消息交互?

按照匹配機(jī)制要求,把消息劃分為 ADD_USER(用戶加入)、MATCH_USER(匹配對(duì)手)、CANCEL_MATCH(取消匹配)、PLAY_GAME(游戲開(kāi)始)、GAME_OVER(游戲結(jié)束)

public enum MessageTypeEnum {

    /**
     * 用戶加入
     */
    ADD_USER,
    /**
     * 匹配對(duì)手
     */
    MATCH_USER,
    /**
     * 取消匹配
     */
    CANCEL_MATCH,
    /**
     * 游戲開(kāi)始
     */
    PLAY_GAME,
    /**
     * 游戲結(jié)束
     */
    GAME_OVER,
}

使用 WebSocket 客戶端可以向服務(wù)端發(fā)送消息,服務(wù)端也能向客戶端發(fā)送消息。把消息按照需求劃分成不同的類(lèi)型,客戶端發(fā)送某一類(lèi)型的消息,服務(wù)端接收后判斷,并按照類(lèi)型分別處理,最后返回向客戶端推送處理結(jié)果。區(qū)別客戶端 WebSocket 連接的是從客戶端傳來(lái)的 userId,用 HashMap 保存

@Component
@Slf4j
@ServerEndpoint(value = "/game/match/{userId}")
public class ChatWebsocket {

    private Session session;

    private String userId;

    static QuestionSev questionSev;
    static MatchCacheUtil matchCacheUtil;

    static Lock lock = new ReentrantLock();

    static Condition matchCond = lock.newCondition();

    @Autowired
    public void setMatchCacheUtil(MatchCacheUtil matchCacheUtil) {
        ChatWebsocket.matchCacheUtil = matchCacheUtil;
    }

    @Autowired
    public void setQuestionSev(QuestionSev questionSev) {
        ChatWebsocket.questionSev = questionSev;
    }

    @OnOpen
    public void onOpen(@PathParam("userId") String userId, Session session) {

        log.info("ChatWebsocket open 有新連接加入 userId: {}", userId);

        this.userId = userId;
        this.session = session;
        matchCacheUtil.addClient(userId, this);

        log.info("ChatWebsocket open 連接建立完成 userId: {}", userId);
    }

    @OnError
    public void onError(Session session, Throwable error) {

        log.error("ChatWebsocket onError 發(fā)生了錯(cuò)誤 userId: {}, errorMessage: {}", userId, error.getMessage());

        matchCacheUtil.removeClinet(userId);
        matchCacheUtil.removeUserOnlineStatus(userId);
        matchCacheUtil.removeUserFromRoom(userId);
        matchCacheUtil.removeUserMatchInfo(userId);

        log.info("ChatWebsocket onError 連接斷開(kāi)完成 userId: {}", userId);
    }

    @OnClose
    public void onClose()
    {
        log.info("ChatWebsocket onClose 連接斷開(kāi) userId: {}", userId);

        matchCacheUtil.removeClinet(userId);
        matchCacheUtil.removeUserOnlineStatus(userId);
        matchCacheUtil.removeUserFromRoom(userId);
        matchCacheUtil.removeUserMatchInfo(userId);

        log.info("ChatWebsocket onClose 連接斷開(kāi)完成 userId: {}", userId);
    }

    @OnMessage
    public void onMessage(String message, Session session) {

        log.info("ChatWebsocket onMessage userId: {}, 來(lái)自客戶端的消息 message: {}", userId, message);

        JSONObject jsonObject = JSON.parseObject(message);
        MessageTypeEnum type = jsonObject.getObject("type", MessageTypeEnum.class);

        log.info("ChatWebsocket onMessage userId: {}, 來(lái)自客戶端的消息類(lèi)型 type: {}", userId, type);

        if (type == MessageTypeEnum.ADD_USER) {
            addUser(jsonObject);
        } else if (type == MessageTypeEnum.MATCH_USER) {
            matchUser(jsonObject);
        } else if (type == MessageTypeEnum.CANCEL_MATCH) {
            cancelMatch(jsonObject);
        } else if (type == MessageTypeEnum.PLAY_GAME) {
            toPlay(jsonObject);
        } else if (type == MessageTypeEnum.GAME_OVER) {
            gameover(jsonObject);
        } else {
            throw new GameServerException(GameServerError.WEBSOCKET_ADD_USER_FAILED);
        }

        log.info("ChatWebsocket onMessage userId: {} 消息接收結(jié)束", userId);
    }

    /**
     * 群發(fā)消息
     */
    private void sendMessageAll(MessageReply<?> messageReply) {

        log.info("ChatWebsocket sendMessageAll 消息群發(fā)開(kāi)始 userId: {}, messageReply: {}", userId, JSON.toJSONString(messageReply));

        Set<String> receivers = messageReply.getChatMessage().getReceivers();
        for (String receiver : receivers) {
            ChatWebsocket client = matchCacheUtil.getClient(receiver);
            client.session.getAsyncRemote().sendText(JSON.toJSONString(messageReply));
        }

        log.info("ChatWebsocket sendMessageAll 消息群發(fā)結(jié)束 userId: {}", userId);
    }

    // 出于減少篇幅的目的,業(yè)務(wù)處理方法暫不貼出...
}

3. 如何保存以及改變用戶狀態(tài)?

創(chuàng)建一個(gè)枚舉類(lèi),定義用戶的狀態(tài)

/**
 * 用戶狀態(tài)
 * @author yeeq
 */
public enum StatusEnum {

    /**
     * 待匹配
     */
    IDLE,
    /**
     * 匹配中
     */
    IN_MATCH,
    /**
     * 游戲中
     */
    IN_GAME,
    /**
     * 游戲結(jié)束
     */
    GAME_OVER,
    ;

    public static StatusEnum getStatusEnum(String status) {
        switch (status) {
            case "IDLE":
                return IDLE;
            case "IN_MATCH":
                return IN_MATCH;
            case "IN_GAME":
                return IN_GAME;
            case "GAME_OVER":
                return GAME_OVER;
            default:
                throw new GameServerException(GameServerError.MESSAGE_TYPE_ERROR);
        }
    }

    public String getValue() {
        return this.name();
    }
}

選擇 Redis 保存用戶狀態(tài),還是創(chuàng)建一個(gè)枚舉類(lèi),Redis 中存儲(chǔ)數(shù)據(jù)都有唯一的 Key 做標(biāo)識(shí),因此在這里定義 Redis 中的 Key,分別介紹如下:

  • USER_STATUS:存儲(chǔ)用戶狀態(tài)的 Key,存儲(chǔ)類(lèi)型是 Map<String, String>,其中用戶 userId 為 key,用戶在線狀態(tài) 為 value
  • USER_MATCH_INFO:當(dāng)用戶處于游戲中時(shí),我們需要記錄用戶的信息,比如分?jǐn)?shù)等。這些信息不需要記錄到數(shù)據(jù)庫(kù),而且隨時(shí)會(huì)更新,放入緩存方便獲取
  • ROOM:可以理解為匹配的兩名用戶創(chuàng)建一個(gè)房間,具體實(shí)現(xiàn)是以鍵值對(duì)方式存儲(chǔ),比如用戶 A 和用戶 B 匹配,用戶 A 的 userId 是 A,用戶 B 的 userId 是 B,則在 Redis 中記錄為 {A -- B},{B -- A}
public enum EnumRedisKey {

    /**
     * userOnline 在線狀態(tài)
     */
    USER_STATUS,
    /**
     * userOnline 對(duì)局信息
     */
    USER_IN_PLAY,
    /**
     * userOnline 匹配信息
     */
    USER_MATCH_INFO,
    /**
     * 房間
     */
    ROOM;

    public String getKey() {
        return this.name();
    }
}

創(chuàng)建一個(gè)工具類(lèi),用于操作 Redis 中的數(shù)據(jù)。

@Component
public class MatchCacheUtil {

    /**
     * 用戶 userId 為 key,ChatWebsocket 為 value
     */
    private static final Map<String, ChatWebsocket> CLIENTS = new HashMap<>();

    /**
     * key 是標(biāo)識(shí)存儲(chǔ)用戶在線狀態(tài)的 EnumRedisKey,value 為 map 類(lèi)型,其中用戶 userId 為 key,用戶在線狀態(tài) 為 value
     */
    @Resource
    private RedisTemplate<String, Map<String, String>> redisTemplate;

    /**
     * 添加客戶端
     */
    public void addClient(String userId, ChatWebsocket websocket) {
        CLIENTS.put(userId, websocket);
    }

    /**
     * 移除客戶端
     */
    public void removeClinet(String userId) {
        CLIENTS.remove(userId);
    }

    /**
     * 獲取客戶端
     */
    public ChatWebsocket getClient(String userId) {
        return CLIENTS.get(userId);
    }

    /**
     * 移除用戶在線狀態(tài)
     */
    public void removeUserOnlineStatus(String userId) {
        redisTemplate.opsForHash().delete(EnumRedisKey.USER_STATUS.getKey(), userId);
    }

    /**
     * 獲取用戶在線狀態(tài)
     */
    public StatusEnum getUserOnlineStatus(String userId) {
        Object status = redisTemplate.opsForHash().get(EnumRedisKey.USER_STATUS.getKey(), userId);
        if (status == null) {
            return null;
        }
        return StatusEnum.getStatusEnum(status.toString());
    }

    /**
     * 設(shè)置用戶為 IDLE 狀態(tài)
     */
    public void setUserIDLE(String userId) {
        removeUserOnlineStatus(userId);
        redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.IDLE.getValue());
    }

    /**
     * 設(shè)置用戶為 IN_MATCH 狀態(tài)
     */
    public void setUserInMatch(String userId) {
        removeUserOnlineStatus(userId);
        redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.IN_MATCH.getValue());
    }

    /**
     * 隨機(jī)獲取處于匹配狀態(tài)的用戶(除了指定用戶外)
     */
    public String getUserInMatchRandom(String userId) {
        Optional<Map.Entry<Object, Object>> any = redisTemplate.opsForHash().entries(EnumRedisKey.USER_STATUS.getKey())
                .entrySet().stream().filter(entry -> entry.getValue().equals(StatusEnum.IN_MATCH.getValue()) && !entry.getKey().equals(userId))
                .findAny();
        return any.map(entry -> entry.getKey().toString()).orElse(null);
    }

    /**
     * 設(shè)置用戶為 IN_GAME 狀態(tài)
     */
    public void setUserInGame(String userId) {
        removeUserOnlineStatus(userId);
        redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.IN_GAME.getValue());
    }

    /**
     * 設(shè)置處于游戲中的用戶在同一房間
     */
    public void setUserInRoom(String userId1, String userId2) {
        redisTemplate.opsForHash().put(EnumRedisKey.ROOM.getKey(), userId1, userId2);
        redisTemplate.opsForHash().put(EnumRedisKey.ROOM.getKey(), userId2, userId1);
    }

    /**
     * 從房間中移除用戶
     */
    public void removeUserFromRoom(String userId) {
        redisTemplate.opsForHash().delete(EnumRedisKey.ROOM.getKey(), userId);
    }

    /**
     * 從房間中獲取用戶
     */
    public String getUserFromRoom(String userId) {
        return redisTemplate.opsForHash().get(EnumRedisKey.ROOM.getKey(), userId).toString();
    }

    /**
     * 設(shè)置處于游戲中的用戶的對(duì)戰(zhàn)信息
     */
    public void setUserMatchInfo(String userId, String userMatchInfo) {
        redisTemplate.opsForHash().put(EnumRedisKey.USER_MATCH_INFO.getKey(), userId, userMatchInfo);
    }

    /**
     * 移除處于游戲中的用戶的對(duì)戰(zhàn)信息
     */
    public void removeUserMatchInfo(String userId) {
        redisTemplate.opsForHash().delete(EnumRedisKey.USER_MATCH_INFO.getKey(), userId);
    }

    /**
     * 設(shè)置處于游戲中的用戶的對(duì)戰(zhàn)信息
     */
    public String getUserMatchInfo(String userId) {
        return redisTemplate.opsForHash().get(EnumRedisKey.USER_MATCH_INFO.getKey(), userId).toString();
    }

    /**
     * 設(shè)置用戶為游戲結(jié)束狀態(tài)
     */
    public synchronized void setUserGameover(String userId) {
        removeUserOnlineStatus(userId);
        redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.GAME_OVER.getValue());
    }
}

4. 如何匹配用戶?

匹配用戶的思路之前已經(jīng)提到過(guò),為了不阻塞客戶端與服務(wù)端的 WebSocket 連接,創(chuàng)建一個(gè)線程專(zhuān)門(mén)用來(lái)匹配用戶,如果匹配成功就向客戶端推送消息

用戶匹配對(duì)手時(shí)遵循這么一個(gè)原則:用戶 A 找到用戶 B,由用戶 A 負(fù)責(zé)一切工作,既由用戶 A 完成創(chuàng)建匹配數(shù)據(jù)并保存到緩存的全部操作。值得注意的一點(diǎn)是,在匹配時(shí)要注意保證狀態(tài)的變化:

  • 當(dāng)前用戶在匹配對(duì)手的同時(shí),被其他用戶匹配,那么當(dāng)前用戶應(yīng)當(dāng)停止匹配操作
  • 當(dāng)前用戶匹配到對(duì)手,但對(duì)手被其他用戶匹配了,那么當(dāng)前用戶應(yīng)該重新尋找新的對(duì)手

用戶匹配對(duì)手的過(guò)程應(yīng)該保證原子性,使用 Java 鎖來(lái)保證

/**
 * 用戶隨機(jī)匹配對(duì)手
 */
@SneakyThrows
private void matchUser(JSONObject jsonObject) {

    log.info("ChatWebsocket matchUser 用戶隨機(jī)匹配對(duì)手開(kāi)始 message: {}, userId: {}", jsonObject.toJSONString(), userId);

    MessageReply<GameMatchInfo> messageReply = new MessageReply<>();
    ChatMessage<GameMatchInfo> result = new ChatMessage<>();
    result.setSender(userId);
    result.setType(MessageTypeEnum.MATCH_USER);

    lock.lock();
    try {
        // 設(shè)置用戶狀態(tài)為匹配中
        matchCacheUtil.setUserInMatch(userId);
        matchCond.signal();
    } finally {
        lock.unlock();
    }

    // 創(chuàng)建一個(gè)異步線程任務(wù),負(fù)責(zé)匹配其他同樣處于匹配狀態(tài)的其他用戶
    Thread matchThread = new Thread(() -> {
        boolean flag = true;
        String receiver = null;
        while (flag) {
            // 獲取除自己以外的其他待匹配用戶
            lock.lock();
            try {
                // 當(dāng)前用戶不處于待匹配狀態(tài)
                if (matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.IN_GAME) == 0
                    || matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.GAME_OVER) == 0) {
                    log.info("ChatWebsocket matchUser 當(dāng)前用戶 {} 已退出匹配", userId);
                    return;
                }
                // 當(dāng)前用戶取消匹配狀態(tài)
                if (matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.IDLE) == 0) {
                    // 當(dāng)前用戶取消匹配
                    messageReply.setCode(MessageCode.CANCEL_MATCH_ERROR.getCode());
                    messageReply.setDesc(MessageCode.CANCEL_MATCH_ERROR.getDesc());
                    Set<String> set = new HashSet<>();
                    set.add(userId);
                    result.setReceivers(set);
                    result.setType(MessageTypeEnum.CANCEL_MATCH);
                    messageReply.setChatMessage(result);
                    log.info("ChatWebsocket matchUser 當(dāng)前用戶 {} 已退出匹配", userId);
                    sendMessageAll(messageReply);
                    return;
                }
                receiver = matchCacheUtil.getUserInMatchRandom(userId);
                if (receiver != null) {
                    // 對(duì)手不處于待匹配狀態(tài)
                    if (matchCacheUtil.getUserOnlineStatus(receiver).compareTo(StatusEnum.IN_MATCH) != 0) {
                        log.info("ChatWebsocket matchUser 當(dāng)前用戶 {}, 匹配對(duì)手 {} 已退出匹配狀態(tài)", userId, receiver);
                    } else {
                        matchCacheUtil.setUserInGame(userId);
                        matchCacheUtil.setUserInGame(receiver);
                        matchCacheUtil.setUserInRoom(userId, receiver);
                        flag = false;
                    }
                } else {
                    // 如果當(dāng)前沒(méi)有待匹配用戶,進(jìn)入等待隊(duì)列
                    try {
                        log.info("ChatWebsocket matchUser 當(dāng)前用戶 {} 無(wú)對(duì)手可匹配", userId);
                        matchCond.await();
                    } catch (InterruptedException e) {
                        log.error("ChatWebsocket matchUser 匹配線程 {} 發(fā)生異常: {}",
                                  Thread.currentThread().getName(), e.getMessage());
                    }
                }
            } finally {
                lock.unlock();
            }
        }

        UserMatchInfo senderInfo = new UserMatchInfo();
        UserMatchInfo receiverInfo = new UserMatchInfo();
        senderInfo.setUserId(userId);
        senderInfo.setScore(0);
        receiverInfo.setUserId(receiver);
        receiverInfo.setScore(0);

        matchCacheUtil.setUserMatchInfo(userId, JSON.toJSONString(senderInfo));
        matchCacheUtil.setUserMatchInfo(receiver, JSON.toJSONString(receiverInfo));

        GameMatchInfo gameMatchInfo = new GameMatchInfo();
        List<Question> questions = questionSev.getAllQuestion();
        gameMatchInfo.setQuestions(questions);
        gameMatchInfo.setSelfInfo(senderInfo);
        gameMatchInfo.setOpponentInfo(receiverInfo);

        messageReply.setCode(MessageCode.SUCCESS.getCode());
        messageReply.setDesc(MessageCode.SUCCESS.getDesc());

        result.setData(gameMatchInfo);
        Set<String> set = new HashSet<>();
        set.add(userId);
        result.setReceivers(set);
        result.setType(MessageTypeEnum.MATCH_USER);
        messageReply.setChatMessage(result);
        sendMessageAll(messageReply);

        gameMatchInfo.setSelfInfo(receiverInfo);
        gameMatchInfo.setOpponentInfo(senderInfo);

        result.setData(gameMatchInfo);
        set.clear();
        set.add(receiver);
        result.setReceivers(set);
        messageReply.setChatMessage(result);

        sendMessageAll(messageReply);

        log.info("ChatWebsocket matchUser 用戶隨機(jī)匹配對(duì)手結(jié)束 messageReply: {}", JSON.toJSONString(messageReply));

    }, CommonField.MATCH_TASK_NAME_PREFIX + userId);
    matchThread.start();
}

項(xiàng)目展示

項(xiàng)目代碼如下:https://github.com/Yee-Q/match-project

跑起來(lái)后,使用 websocket-client 可以進(jìn)行測(cè)試。在瀏覽器打開(kāi),在控制臺(tái)查看消息。

在連接輸入框隨便輸入一個(gè)數(shù)字作為 userId,點(diǎn)擊連接,此時(shí)客戶端就和服務(wù)端建立 WebSocket 連接了

點(diǎn)擊加入用戶按鈕,用戶“進(jìn)入匹配大廳”

點(diǎn)擊隨機(jī)匹配按鈕,開(kāi)始匹配,再取消匹配

按照之前的步驟再建立一個(gè)用戶連接,都點(diǎn)擊隨機(jī)匹配按鈕,匹配成功,服務(wù)端返回響應(yīng)信息

用戶分?jǐn)?shù)更新時(shí),在輸入框輸入新的分?jǐn)?shù),比如 6,點(diǎn)擊實(shí)時(shí)更新按鈕,對(duì)手將受到最新的分?jǐn)?shù)消息

當(dāng)雙方都點(diǎn)擊游戲結(jié)束按鈕,則游戲結(jié)束

以上就是SpringBoot + WebSocket 實(shí)現(xiàn)答題對(duì)戰(zhàn)匹配機(jī)制案例詳解的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot WebSocket答題對(duì)戰(zhàn)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論

朝阳区| 华阴市| 长汀县| 胶南市| 灵石县| 沂源县| 罗平县| 民丰县| 牙克石市| 镇远县| 内丘县| 淮北市| 潢川县| 金溪县| 南丰县| 台州市| 深圳市| 韶山市| 当阳市| 恩施市| 濮阳县| 阿坝县| 田东县| 长顺县| 嘉鱼县| 鹰潭市| 荣成市| 凌海市| 屏东市| 米泉市| 沐川县| 靖安县| 泊头市| 聂拉木县| 共和县| 措勤县| 鹤峰县| 太仆寺旗| 合水县| 河池市| 红桥区|