Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 506, comments - 13, trackbacks - 0, articles - 1

          本文由蘇三說技術(shù)分享,原題“微信群聊功能,原來是這樣設計的!”,下文進行了排版和內(nèi)容優(yōu)化等。

          1、引言

          當我那天拿著手機,正在和朋友們的微信群里暢聊著八卦新聞和即將到來的周末計劃時,忽然一條帶著喜意的消息撲面而來,消息正中間寫著八個大字:恭喜發(fā)財,大吉大利。

          搶紅包!!相信大部分人對此都不陌生,微信的這個群聊系統(tǒng)可以方便地聊天、分享圖片和表情,還有那個神奇的紅包功能。

          微信作為 10 億用戶級別的全民 App,微信建群功能是微信里面核心的一個能力,它可以將數(shù)百個好友或陌生人放進一個群空間。

          微信背后的這個IM群聊系統(tǒng)到底是如何實現(xiàn)的呢?這個問題一直困擾著,于是我決定深入了解一下,看看微信的群聊系統(tǒng)背后的設計是怎樣的。

          技術(shù)交流:

          - 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM

          - 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK備用地址點此

          (本文已同步發(fā)布于:http://www.52im.net/thread-4635-1-1.html

          2、群聊系統(tǒng)需求

          2.1系統(tǒng)特點與功能需求

          微信群聊功能是社交應用的核心功能之一,它允許用戶創(chuàng)建自己的社交圈子,與家人、朋友或共同興趣愛好者進行友好地交流。

          以下是微信群聊系統(tǒng)的核心功能:

          具體來說就是:

          • 1)創(chuàng)建群聊:用戶可以創(chuàng)建新的聊天群組,邀請其他好友用戶加入或與陌生人面對面建群;
          • 2)群組管理:群主和管理員能夠管理群成員,設置規(guī)則和權(quán)限;
          • 3)消息發(fā)送和接收:允許群成員發(fā)送文本、圖片、音頻、視頻等多種類型的消息,并推送給所有群成員;
          • 4)實時通信:消息應該能夠快速傳遞,確保實時互動;
          • 5)搶紅包:用戶在群聊中發(fā)送任意個數(shù)和金額的紅包,群成員可以搶到隨機金額的紅包。

          2.2非功能需求(應對高并發(fā)、高性能、海量存儲)

          當我們面對 10 億微信用戶每天都可能使用建群功能的情景時,就需要處理大規(guī)模的用戶并發(fā)。這就引出了系統(tǒng)的非功能需求。

          主要包括以下方面:

          • 1)高并發(fā):系統(tǒng)需要支持大量用戶同時創(chuàng)建和使用群組,以確保無延遲的用戶體驗;
          • 2)高性能:快速消息傳遞、即時響應,是數(shù)字社交的關(guān)鍵;
          • 3)海量存儲:系統(tǒng)必須可擴展,以容納用戶生成的海量消息文本、圖片及音視頻數(shù)據(jù)。

          3、群聊概要設計

          在概要設計中,我們考慮了系統(tǒng)的核心組件和基本業(yè)務的概要設計。

          3.1核心組件

          微信群聊系統(tǒng)中,會涉及到如下核心組件和協(xié)議:

          具體就是:

          • 1)客戶端:接收手機或 PC 端微信群聊的消息,并實時傳輸給后臺服務器;
          • 2)Websocket傳輸協(xié)議:支持客戶端和后臺服務端的實時交互,開銷低,實時性高,常用于微信、QQ 等 IM 系統(tǒng)通信系統(tǒng);
          • 3)長連接集群:與客戶端進行 Websocket 長連接的系統(tǒng)集群,并將消息通過中間件轉(zhuǎn)發(fā)到應用服務器;
          • 4)消息處理服務器集群:提供實時消息的處理能力,包括數(shù)據(jù)存儲、查詢、與數(shù)據(jù)庫交互等;
          • 5)消息推送服務器集群:這是信息的中轉(zhuǎn)站,負責將消息傳遞給正確的群組成員;
          • 6)數(shù)據(jù)庫服務器集群:用于存儲用戶文本數(shù)據(jù)、圖片的縮略圖、音視頻元數(shù)據(jù)等;
          • 7)分布式文件存儲集群:存儲用戶圖片、音視頻等文件數(shù)據(jù)。

          3.2業(yè)務概要設計

          群聊創(chuàng)建主要涉及以下這些事:

          1)唯一ID分配:當用戶請求創(chuàng)建一個新群組時,系統(tǒng)生成一個唯一的群組 ID,通常可以使用分布式 ID 生成器如雪花算法(Snowflake)或直接使用數(shù)據(jù)庫自增 ID。這里我們?yōu)榱藢崿F(xiàn)簡便,采用 MySQL 的自增 ID。

          PS:其實IM里消息ID的生成也是個很熱門的技術(shù)點,有興趣可以閱讀下面的文章:

          1. IM消息ID技術(shù)專題(一):微信的海量IM聊天消息序列號生成實踐(算法原理篇)
          2. IM消息ID技術(shù)專題(二):微信的海量IM聊天消息序列號生成實踐(容災方案篇)
          3. IM消息ID技術(shù)專題(三):解密融云IM產(chǎn)品的聊天消息ID生成策略
          4. IM消息ID技術(shù)專題(四):深度解密美團的分布式ID生成算法
          5. IM消息ID技術(shù)專題(五):開源分布式ID生成器UidGenerator的技術(shù)實現(xiàn)
          6. IM消息ID技術(shù)專題(六):深度解密滴滴的高性能ID生成器(Tinyid)
          7. IM消息ID技術(shù)專題(七):深度解密vivo的自研分布式ID服務(魯班)

          2)群組信息存儲:將群組 ID 和相關(guān)信息(例如群名、創(chuàng)建者 ID 等)存儲在群組數(shù)據(jù)庫中。

          3)成員關(guān)聯(lián):將群主添加為群組的創(chuàng)始成員,同時創(chuàng)建者也會成為管理員。

          4)消息歷史記錄:為了確保新成員能夠訪問以前的消息,將此新群組的群組 ID 與用戶消息關(guān)聯(lián)存儲。

          除了拉好友建群,微信還實現(xiàn)了面對面建群的能力。

          接下來,我們深入探討了三到四個核心功能的詳細設計,包括面對面建群、消息發(fā)送與接收及搶紅包功能。

          4、面對面建群功能

          4.1概述

          用戶發(fā)起面對面建群,并輸入一個 4 位數(shù)的隨機碼,周圍的用戶輸入該隨機碼后可加入群聊,面對面建群功能通常涉及數(shù)據(jù)表設計和核心業(yè)務交互流程將在下面的小節(jié)里詳細逐個討論。

          4.2數(shù)據(jù)庫表設計

          涉及到群聊的主要數(shù)據(jù)庫表有:

          • 1)User 表:存儲用戶信息,包括用戶 ID、昵稱、頭像等;
          • 2)Group 表:存儲群組信息,包括群 ID、群名稱、創(chuàng)建者 ID、群成員個數(shù)等;
          • 3)GroupMember 表:關(guān)聯(lián)用戶和群組,包括用戶 ID 和群 ID;
          • 4)RandomCode 表:存儲面對面建群的隨機碼和關(guān)聯(lián)的群 ID。

          4.3核心業(yè)務交互流程

          用戶 A 在手機端應用中發(fā)起面對面建群,并輸入一個隨機碼,校驗通過后,等待周圍(50 米之內(nèi))的用戶加入。此時,系統(tǒng)將用戶信息以 HashMap 的方式存入緩存中,并設置過期時間為 3min。

          1{隨機碼,用戶列表[用戶A(ID、名稱、頭像)]}

          用戶 B 在另一個手機端發(fā)起面對面建群,輸入指定的隨機碼,如果該用戶周圍有這樣的隨機碼,則進入同一個群聊等待頁面,并可以看到其它群員的頭像和昵稱信息。

          此時,系統(tǒng)除了根據(jù)隨機碼獲取所有用戶信息,也會實時更新緩存里的用戶信息。

          當?shù)谝粋€用戶點擊進入該群時,就可以加入群聊,系統(tǒng)將生成的隨機碼保存在 RandomCode 表中,并關(guān)聯(lián)到新創(chuàng)建的群 ID,更新群成員的個數(shù)。

          然后,系統(tǒng)將用戶信息和新生成的群聊信息存儲在 Group、GroupMember 表中。

          4.4成員加入,刷新群員信息

          之后:B、C 用戶帶著隨機碼加入群聊時,手機客戶端向服務器后端發(fā)送請求,驗證隨機碼是否有效。服務器后端驗證隨機碼,檢查隨機碼是否存在于緩存中,以及是否在有效期內(nèi)。

          然后:判斷當前群成員是否滿員(目前普通用戶創(chuàng)建的群聊人數(shù)最多為 500 人),如果驗證通過,服務器后端將用戶 B、C 添加到群成員表 GroupMember 中,并返回成功響應。

          移動客戶端應用收到成功響應后,更新用戶 B、C 的群聊列表,展示他們已加入的新群聊。

          4.5其它技術(shù)組件

          這樣,用戶 A 通過創(chuàng)建隨機碼和周圍的用戶掃描二維碼的方式成功建立了一個面對面建群。這個功能涉及了多個技術(shù)組件,包括分布式緩存、數(shù)據(jù)庫、二維碼生成和驗證等。

          同時,在面對面建群的過程中相當重要的能力是標識用戶的區(qū)域,比如 50 米以內(nèi)。這個可以用到 Redis 的 GeoHash 算法,來獲取一個范圍內(nèi)的所有用戶信息。

          5、 群聊消息發(fā)送與接收能力

          5.1概述

          當某個成員在微信群里發(fā)言,系統(tǒng)需要處理消息的分發(fā)、通知其他成員、以及確保消息的顯示。以下是這一功能的詳細交互步驟,以及數(shù)據(jù)庫存儲方案。

          5.2交互流程

          消息發(fā)送和接收時序圖如下:

          具體就是:

          1)用戶A在群中發(fā)送一條帶有圖片、視頻或音頻的消息。

          2)移動客戶端應用將消息內(nèi)容和媒體文件上傳到服務器后端。

          3)服務器后端接收到消息和媒體文件后,將消息內(nèi)容存儲到 Message 表中,同時將媒體文件存儲到分布式文件存儲集群中。在 Message 表里,不僅記錄了媒體文件的 MediaID,以便關(guān)聯(lián)消息和媒體;還記錄了縮略圖、視頻封面圖等等。

          4)服務器后端會向所有群成員廣播這條消息。移動客戶端應用接收到消息后,會根據(jù)消息類型(文本、圖片、視頻、音頻)加載對應的展示方式。

          5)當用戶點擊查看圖片、視頻或音頻縮略圖時,客戶端應用會根據(jù) MediaID 到對象存儲集群中獲取對應的媒體文件路徑,并將其展示給用戶。

          這個流程確保了消息和媒體文件的有效存儲和展示。用戶可以上傳和查看各種類型的媒體數(shù)據(jù),而服務器后端通過關(guān)聯(lián) Message 和對象存儲服務器中的信息,實現(xiàn)了有效的消息存儲和展示。

          5.3消息存儲和展示

          在微信群中保存和展示用戶的圖片、視頻或音頻數(shù)據(jù),通常需要進行數(shù)據(jù)存儲和展示方面的設計。除了上面面對面建群功能中提到的用戶表和群組表以外,還需要以下表結(jié)構(gòu)。

          比如:

          1)Message表: 用于存儲消息,每個消息都有一個唯一的 MessageID,消息類型(文本、圖片、視頻、音頻),消息內(nèi)容(文字、圖片縮略圖、視頻封面圖等),發(fā)送者 UserID、接收群 GroupID、發(fā)送時間等字段。

          2)Media表: 存儲用戶上傳的圖片、視頻、音頻等媒體數(shù)據(jù)。每個媒體文件都有一個唯一的 MediaID,文件路徑、上傳者 UserID、上傳時間等字段。

          3)MessageState表: 用于存儲用戶消息狀態(tài),包括 MessageID、用戶 ID、是否已讀等。在消息推送時,通過這張表計算未讀數(shù),統(tǒng)一推送給用戶,并在離線用戶的手機上展示一個小數(shù)字代表消息未讀數(shù)。

          我們知道,MySQL 每次查詢 select count 類型的語句時,都會觸發(fā)全表掃描,所以每次加載消息未讀數(shù)都很慢。

          為了查詢性能考慮,我們可以將用戶的消息數(shù)量存入 Redis,并實時記錄一個未讀數(shù)值。并且,當未讀數(shù)大于 99 時,就將未讀數(shù)值置為 100 且不再增加。

          當推送用戶消息時,只要未讀數(shù)為 100,就將推送消息數(shù)設置為 99+,以此來提升存儲的性能和交互的效率。

          6、 群聊中的搶紅包功能

          6.1概述

          搶紅包功能允許用戶在群聊中發(fā)送任意個數(shù)和金額的紅包,群成員可以搶到隨機金額的紅包,但要保證每個用戶的紅包金額不小于 0.01 元。

          搶紅包的詳細交互流程如下:

          • 1)用戶接收到搶紅包通知,點擊通知打開群聊頁面;
          • 2)用戶點擊搶紅包,后臺服務驗證用戶資格,確保用戶尚未領(lǐng)取過此紅包;
          • 3)若用戶資格驗證通過,后臺服務分配紅包金額并存儲領(lǐng)取記錄;
          • 4)用戶在微信群中看到領(lǐng)取金額,紅包狀態(tài)更新為“已領(lǐng)取”;
          • 5)異步調(diào)用支付接口,將紅包金額更新到錢包里。

          搶紅包功能需要關(guān)注搶紅包的數(shù)據(jù)庫設計,搶紅包實時性和紅包分配算法。

          6.2數(shù)據(jù)庫設計

          紅包表 redpack 的字段如下:

          • 1)id: 主鍵,紅包ID;
          • 2)totalAmount: 總金額;
          • 3)surplusAmount: 剩余金額;
          • 4)total: 紅包總數(shù);
          • 5)surplusTotal: 剩余紅包總數(shù);
          • 6)userId: 發(fā)紅包的用戶ID。

          該表用來記錄用戶發(fā)了多少紅包,以及需要維護的剩余金額。

          紅包記錄表 redpack_record 如下:

          • 1)id: 主鍵,記錄ID;
          • 2)redpackId: 紅包ID,外鍵;
          • 3)userId: 用戶ID;
          • 4)amount: 搶到的金額。

          記錄表用來存放用戶具體搶到的紅包信息,也是紅包表的副表。

          6.3實時性

          發(fā)紅包的步驟:

          • 1)用戶設置紅包的總金額和個數(shù)后,在紅包表中增加一條數(shù)據(jù),開始發(fā)紅包;
          • 2)為了保證實時性和搶紅包的效率,在 Redis 中增加一條記錄,存儲紅包 ID 和總?cè)藬?shù) n;
          • 3)搶紅包消息推送給所有群成員。

          從 2015 年起,微信紅包的搶紅包和拆紅包就分離了,用戶點擊搶紅包后需要進行兩次操作。這也是為什么明明有時候搶到了紅包,點開后卻發(fā)現(xiàn)該紅包已經(jīng)被領(lǐng)取完了。

          搶紅包的交互步驟如下:

          • 1)搶紅包:搶操作在 Redis 緩存層完成,通過原子遞減的操作來更新紅包個數(shù),到 0 后就說明搶光了;
          • 2)拆紅包:拆紅包時,首先會實時計算金額,一般是通過二倍均值法實現(xiàn)(即 0.01 到剩余平均值的 2 倍之間);
          • 3)紅包記錄:用戶獲取紅包金額后,通過數(shù)據(jù)庫的事務操作累加已經(jīng)領(lǐng)取的個數(shù)和金額,并更新紅包表和記錄表;
          • 4)轉(zhuǎn)賬:為了提升效率,最終的轉(zhuǎn)賬為異步操作,這也是為什么在春節(jié)期間,紅包領(lǐng)取后不能立即在余額中看到的原因。

          6.4紅包分配算法

          紅包金額分配時,由于是隨機分配,所以有兩種實現(xiàn)方案:

          • 1)實時拆分;
          • 2)預先生成。

          6.4.1)實時拆分:

          實時拆分,指的是在搶紅包時實時計算每個紅包的金額,以實現(xiàn)紅包的拆分過程。

          這個需要我們設計一個好的拆分算法,讓紅包拆分時一直保證后續(xù)待拆分紅包的金額不能為空。

          實時拆分時,不容易做到拆分的紅包金額服從正態(tài)分布規(guī)律。

          6.4.2)預先生成:

          預先生成,指的是在紅包開搶之前已經(jīng)完成了紅包的金額拆分,搶紅包時只是依次取出拆分好的紅包金額。

          這種方式對拆分算法要求較低,可以拆分出隨機性很好的紅包金額,但通常需要結(jié)合隊列使用,而且需要多設計一個表來存儲紅包的拆分金額。

          6.4.3)二倍均值法:

          綜合上述優(yōu)缺點考慮,以及微信群聊中的人數(shù)不多(目前最高 500 人),所以我們采用實時拆分的方式,用二倍均值法來生成隨機紅包,只滿足隨機即可,不需要正態(tài)分布。

          故可能出現(xiàn)很大的紅包差額,但這更刺激不是嗎🐶。。。

          使用二倍均值法生成的隨機數(shù),每次隨機金額會在 0.01 ~ 剩余平均值*2 之間。

          假設當前紅包剩余金額為 10 元,剩余個數(shù)為 5,10/5 = 2,則當前用戶可以搶到的紅包金額為:0.01 ~ 4 元之間。

          6.4.4)算法優(yōu)化

          用二倍均值法生成的隨機紅包雖然接近平均值,但之前我在某論壇上看到過類似的說法:微信紅包金額的隨機性和領(lǐng)取的時機有關(guān)系,尤其是金額不高的情況下。

          于是,小?耗費巨資在微信群發(fā)了多個紅包,得出了這樣一個結(jié)論:如果發(fā)出的 紅包總額 = 紅包數(shù)*0.01 + 0.01,比如:發(fā)了 4 個紅包,總額為 0.05,則最后一個人領(lǐng)取的紅包金額一定是 0.02。

          無一例外:

          所以,紅包金額算法大概率不是隨機分配,而是在派發(fā)紅包之前已經(jīng)做了處理。比如在紅包金額生成前,先生成一個不存在的紅包,這個紅包的總額為 0.01 * 紅包總數(shù)。

          而在紅包金額分配的時候,會對每個紅包的隨機值基礎上加上 0.01,以此來保證每個紅包的最小值不為 0。

          所以,假設用戶發(fā)了總額為 0.04 的個數(shù)為 3 的紅包時,需要先提取 3*0.01 到 "第四個" 不存在的紅包里面,于是第一個人搶到的紅包隨機值是 0 ~ (0.04-3*0.01)/3。

          由于擔心紅包超額,所以除數(shù)的商是向下取二位小數(shù),0 ~ (0.04-3*0.01)/3 ==> (0 ~ 0) = 0,再加上之前提取的保底值 0.01,于是前兩個搶到的紅包金額都是 0.01。最后一個紅包的金額為紅包余額,即 0.02。

          算法邏輯用 Go 語言實現(xiàn)如下:

          import(

             "fmt"

             "math"

             "math/rand"

             "strconv"

          )

           

          typeRedPack struct{

           

              SurplusAmount float64// 剩余金額

           

              SurplusTotal int// 紅包剩余個數(shù)

           

          }

           

          // 取兩位小數(shù)

          funcremainTwoDecimal(num float64) float64{

           

              numStr := strconv.FormatFloat(num, 'f', 2, 64)

           

              num, _ = strconv.ParseFloat(numStr, 64)

              returnnum

          }

           

          // 獲取隨機金額的紅包

          funcgetRandomRedPack(rp *RedPack) float64{

              ifrp.SurplusTotal <= 0 {

                  // 該紅包已經(jīng)被搶完了

                  return0

              }

           

              ifrp.SurplusTotal == 1 {

                  returnremainTwoDecimal(rp.SurplusAmount + 0.01)

              }

           

                 // 向下取整

              avgAmount := math.Floor(100*(rp.SurplusAmount/float64(rp.SurplusTotal))) / float64(100)

              avgAmount = remainTwoDecimal(avgAmount)

           

                 // 生成隨機數(shù)種子

              rand.NewSource(time.Now().UnixNano())

           

              varmax float64

              ifavgAmount > 0 {

                  max = 2*avgAmount - 0.01

              } else{

                  max = 0

              }

              money := remainTwoDecimal(rand.Float64()*(max) + 0.01)

           

              rp.SurplusTotal -= 1

              rp.SurplusAmount = remainTwoDecimal(rp.SurplusAmount + 0.01 - money)

           

              returnmoney

          }

           

          funcmain() {

              rp := &RedPack{

                  SurplusAmount: 0.06,

                  SurplusTotal:  5,

              }

           

              // 每個紅包先保留 0.01 的余額

           

              rp.SurplusAmount -= 0.01 * float64(rp.SurplusTotal)

              total := rp.SurplusTotal

              fori := 0; i < total; i++ {

                  fmt.Println(getRandomRedPack(rp))

              }

          }

          打印結(jié)果:

          10.01、0.01、0.01、0.01、0.02

          符合預期!

          PS:另一篇《社交軟件紅包技術(shù)解密(十一):解密微信紅包隨機算法(含代碼實現(xiàn))》,也可以一并閱讀。

          7、 本文小結(jié)

          微信群聊及搶紅包等功能背后蘊藏著復雜的交互技術(shù)和精心設計的產(chǎn)品體驗,通過這些核心組件、數(shù)據(jù)庫表和詳細的交互流程,讓用戶能夠輕松參與并享受群聊系統(tǒng)帶來的便利。

          并且,添加了這些充滿趣味的功能,也是微信用戶眾多的原因之一吧!

          微信建群功能的系統(tǒng)設計不僅僅是一個技術(shù)壯麗的展示,更是數(shù)字社交的魔法之一。

          8、 參考資料

          [1] IM單聊和群聊中的在線狀態(tài)同步應該用“推”還是“拉”?

          [2] IM群聊消息如此復雜,如何保證不丟不重?

          [3] 移動端IM中大規(guī)模群消息的推送如何保證效率、實時性?

          [4] IM群聊消息究竟是存1份(即擴散讀)還是存多份(即擴散寫)?

          [5] 一套高可用、易伸縮、高并發(fā)的IM群聊、單聊架構(gòu)方案設計實踐

          [6] 網(wǎng)易云信技術(shù)分享:IM中的萬人群聊技術(shù)方案實踐總結(jié)

          [7] 阿里釘釘技術(shù)分享:企業(yè)級IM王者——釘釘在后端架構(gòu)上的過人之處

          [8] IM群聊消息的已讀未讀功能在存儲空間方面的實現(xiàn)思路探討

          [9] 直播系統(tǒng)聊天技術(shù)(二):阿里電商IM消息平臺,在群聊、直播場景下的技術(shù)實踐

          [10] 直播系統(tǒng)聊天技術(shù)(七):直播間海量聊天消息的架構(gòu)設計難點實踐

          [11] 企業(yè)微信的IM架構(gòu)設計揭秘:消息模型、萬人群、已讀回執(zhí)、消息撤回等

          [12] 融云IM技術(shù)分享:萬人群聊消息投遞方案的思考和實踐

          [13] 實時社群技術(shù)專題(一):支持百萬人超級群聊,一文讀懂社群產(chǎn)品Discord

          [14] 社交軟件紅包技術(shù)解密(一):全面解密QQ紅包技術(shù)方案——架構(gòu)、技術(shù)實現(xiàn)等

          [15] 社交軟件紅包技術(shù)解密(十一):解密微信紅包隨機算法(含代碼實現(xiàn))

          (本文已同步發(fā)布于:http://www.52im.net/thread-4635-1-1.html



          作者:Jack Jiang (點擊作者姓名進入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時通訊開發(fā)交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時是【原創(chuàng)Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 三台县| 普兰店市| 平湖市| 玛纳斯县| 类乌齐县| 独山县| 临西县| 始兴县| 龙胜| 河北区| 井冈山市| 梁河县| 峨眉山市| 抚顺市| 铁力市| 抚松县| 四平市| 津南区| 高平市| 台南县| 屯门区| 黑水县| 株洲市| 来宾市| 湖州市| 新晃| 静宁县| 大冶市| 盘山县| 防城港市| 凌源市| 石棉县| 三河市| 涟源市| 托克逊县| 丹阳市| 汕尾市| 呈贡县| 伊川县| 眉山市| 洱源县|