我的CSDN: ListerCi
我的簡(jiǎn)書(shū): 東方未曦
前言
這是秋招前做的一個(gè)應(yīng)用似舵,當(dāng)時(shí)是想通過(guò)一個(gè)完整的項(xiàng)目來(lái)向面試官展現(xiàn)項(xiàng)目設(shè)計(jì)能力和實(shí)戰(zhàn)能力绅喉,不過(guò)直到秋招最后很多面試官都沒(méi)問(wèn)這個(gè)禽额,想來(lái)是他們覺(jué)得一個(gè)應(yīng)屆生做的東西也不會(huì)包含什么高深的技術(shù)吧下硕。╮(╯▽╰)╭
在本次應(yīng)用的開(kāi)發(fā)過(guò)程中也遇到了很多問(wèn)題线梗,基本都是通過(guò)查閱博客解決的缚俏,在此感謝各位分享技術(shù)的博客大佬惊搏。因此現(xiàn)在我也將應(yīng)用的實(shí)現(xiàn)過(guò)程記錄下來(lái),希望可以幫助到有需要的人忧换。如果博文里有沒(méi)有解釋清楚的地方可以在評(píng)論區(qū)告知恬惯,我會(huì)再做補(bǔ)充。
一亚茬、系統(tǒng)截圖及說(shuō)明
1. 首頁(yè)
首頁(yè)主要顯示背景圖和好友動(dòng)態(tài)酪耳,采用CoordinatorLayout布局來(lái)實(shí)現(xiàn)類(lèi)似微信朋友圈的效果,上方顯示用戶(hù)自己設(shè)置的背景圖刹缝,當(dāng)用戶(hù)在首頁(yè)向下滑動(dòng)時(shí)會(huì)逐漸關(guān)閉背景圖并顯示好友動(dòng)態(tài)碗暗,向上滑動(dòng)到頂部時(shí)又會(huì)逐漸顯示背景圖。
背景圖下方是可以左右滑動(dòng)的“圈內(nèi)動(dòng)態(tài)”和“公開(kāi)動(dòng)態(tài)”標(biāo)簽梢夯,本來(lái)的想法是如果發(fā)表圈內(nèi)動(dòng)態(tài)言疗,只有好友可見(jiàn);如果發(fā)表公開(kāi)動(dòng)態(tài)颂砸,則所有用戶(hù)可見(jiàn)噪奄。不過(guò)最后沒(méi)有實(shí)現(xiàn)公開(kāi)動(dòng)態(tài)的功能,只有圈內(nèi)動(dòng)態(tài)人乓,因此必須是好友才能互相查看動(dòng)態(tài)勤篮。
2. 動(dòng)態(tài)詳情
動(dòng)態(tài)詳情包括文字及圖片,系統(tǒng)設(shè)定最多支持6張圖片色罚。在動(dòng)態(tài)詳情頁(yè)下方還有點(diǎn)贊數(shù)量以及其他用戶(hù)的評(píng)論碰缔。如果當(dāng)前用戶(hù)點(diǎn)贊了這條動(dòng)態(tài),點(diǎn)贊圖標(biāo)就會(huì)變?yōu)榧t色戳护。
3. 好友
點(diǎn)擊屏幕下方的“好友”選項(xiàng)卡可以查看用戶(hù)的所有好友金抡。
在該頁(yè)面點(diǎn)擊上方的“查找用戶(hù)”欄跳轉(zhuǎn)到搜索頁(yè)面,輸入用戶(hù)的賬號(hào)可以進(jìn)行精確查詢(xún)姑尺,輸入用戶(hù)名可以進(jìn)行模糊查詢(xún)竟终。
點(diǎn)擊“好友請(qǐng)求”欄可以查看其它用戶(hù)發(fā)送的好友請(qǐng)求,點(diǎn)擊同意之后切蟋,該請(qǐng)求的狀態(tài)就會(huì)變?yōu)椤耙淹狻薄?/p>
4. 個(gè)人界面
點(diǎn)擊屏幕下方的“我”切換到個(gè)人頁(yè)面。
點(diǎn)擊個(gè)人界面中的“設(shè)置”欄可以前往修改頭像榆芦、用戶(hù)名和個(gè)性簽名柄粹。
5. 發(fā)表動(dòng)態(tài)
首頁(yè)拉到最上方喘鸟,點(diǎn)擊右上角的圖標(biāo)(見(jiàn)圖1)可以發(fā)送動(dòng)態(tài)。
動(dòng)態(tài)包括圖片和文字驻右。根據(jù)設(shè)計(jì)什黑,圖片最多添加6張,因此在添加圖片時(shí)需要對(duì)圖片數(shù)量進(jìn)行限制堪夭。選擇圖片時(shí)右上角會(huì)顯示當(dāng)前已經(jīng)選擇的圖片數(shù)量和最大數(shù)量愕把。
點(diǎn)擊完成后,可以在發(fā)布頁(yè)面看到已經(jīng)選擇的圖片的預(yù)覽森爽。
此時(shí)用戶(hù)可以再次點(diǎn)擊添加圖片的按鈕恨豁,當(dāng)前的圖片最大選擇數(shù)量變?yōu)榱?,以保證不管用戶(hù)分幾次添加圖片爬迟,圖片的數(shù)量都不會(huì)超過(guò)6橘蜜。
點(diǎn)擊完成,預(yù)覽界面如下所示付呕。
二计福、環(huán)境及配置
1. MySQL5.7
鑒于應(yīng)用是支持中文的,數(shù)據(jù)庫(kù)的編碼肯定要修改為utf-8徽职,不過(guò)數(shù)據(jù)庫(kù)的utf-8編碼最多占用3個(gè)字節(jié)象颖,無(wú)法支持占用字節(jié)更多的emoji,怎么辦姆钉?幸好從MySQL5.5.3開(kāi)始说订,數(shù)據(jù)庫(kù)提供了一種新的編碼,那就是utf8mb4育韩。因此首先需要將數(shù)據(jù)庫(kù)的編碼進(jìn)行修改克蚂,修改完數(shù)據(jù)庫(kù)的編碼后,還需要對(duì)表中字段的編碼格式進(jìn)行修改筋讨。具體如下所示:
(1)修改mysql配置文件my.cnf(windows為my.ini)
my.cnf一般在etc/mysql/my.cnf位置埃叭。找到后請(qǐng)?jiān)谝韵氯糠掷锾砑尤缦聝?nèi)容:
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init_connect='SET NAMES utf8mb4'
隨后重啟數(shù)據(jù)庫(kù),通過(guò)命令SHOW VARIABLES WHERE Variable_name LIKE 'character_set_%' OR Variable_name LIKE 'collation%';
檢查編碼悉罕,得到如下結(jié)果表示成功赤屋。
Variable_name | Value |
---|---|
character_set_client | utf8mb4 |
character_set_connection | utf8mb4 |
character_set_database | utf8mb4 |
character_set_filesystem | binary |
character_set_results | utf8mb4 |
character_set_server | utf8mb4 |
character_set_system | utf8 |
character_sets_dir | C:\Program Files\MySQL\MySQL Server 5.7\share\charsets\ |
collation_connection | utf8mb4_unicode_ci |
collation_database | utf8mb4_unicode_ci |
collation_server | utf8mb4_unicode_ci |
(2)修改數(shù)據(jù)庫(kù)表和表中的字段的編碼格式
目前還未建表,先講一下修改編碼格式的方法壁袄,建表之后可以根據(jù)需要修改某些字段的編碼类早。本應(yīng)用中支持emoji的字段為:動(dòng)態(tài)文字、動(dòng)態(tài)評(píng)論嗜逻、用戶(hù)名和用戶(hù)簽名涩僻。
首先需要修改表的編碼格式:
ALTER TABLE `tablename` DEFAULT CHARACTER SET utf8mb4;
之后需要修改特定字段的編碼格式:
ALTER TABLE `tablename` CHANGE `字段名` `字段名` VARCHAR(36) CHARACTER SET utf8mb4 NOT NULL;
如果要將整個(gè)表的字段編碼修改,則使用如下語(yǔ)句:
alter table `tablename` convert to character set utf8mb4;
2. Eclipse中添加服務(wù)器包
本應(yīng)用的服務(wù)器接口使用Servlet編寫(xiě),熟悉框架的朋友可以使用Spring逆日。
在向服務(wù)器添加庫(kù)的時(shí)候嵌巷,有的朋友會(huì)從Tomcat下復(fù)制jar包到項(xiàng)目的lib路徑下,但是最好的辦法是將服務(wù)器的環(huán)境直接添加室抽。
在Eclipse點(diǎn)擊上方選項(xiàng)欄的Project->Properties->Java Build Path右方Add Library->Server Runtime選擇Tomcat對(duì)應(yīng)的版本即可搪哪。
3. 云服務(wù)器及應(yīng)用部署
本次應(yīng)用使用的是阿里云服務(wù)器,如果是學(xué)生的話(huà)坪圾,直接使用阿里云學(xué)生特權(quán)購(gòu)買(mǎi)即可晓折,大概¥120一年。在云服務(wù)器上需要安裝JDK8兽泄、Tomcat以及MySQL數(shù)據(jù)庫(kù)漓概。
在本地編寫(xiě)完服務(wù)器程序后打包成war包放置到服務(wù)器tomcat/webapps/目錄下,隨后在tomcat/bin/目錄下點(diǎn)擊startup即可啟動(dòng)服務(wù)器已日。
三垛耳、系統(tǒng)功能模塊劃分
1. 注冊(cè)登錄
注冊(cè)較為簡(jiǎn)單,用戶(hù)在客戶(hù)端填寫(xiě)賬號(hào)和密碼即可飘千,系統(tǒng)會(huì)為該用戶(hù)安排一個(gè)默認(rèn)的用戶(hù)名和頭像堂鲜。
用戶(hù)使用注冊(cè)的賬號(hào)密碼登錄之后,客戶(hù)端會(huì)在本地保存登錄憑證护奈,以便用戶(hù)下一次打開(kāi)APP時(shí)免去登錄缔莲。
2. 用戶(hù)信息管理
信息管理主要為背景圖修改、頭像修改霉旗、用戶(hù)名修改痴奏、個(gè)性簽名修改。這些信息都存儲(chǔ)在user表中厌秒,用戶(hù)信息管理都是修改user表中的字段读拆。
3. 添加好友
用戶(hù)可以通過(guò)搜索賬號(hào)或用戶(hù)名來(lái)檢索用戶(hù),通過(guò)賬號(hào)搜索時(shí)為精確查詢(xún)鸵闪,輸入的賬號(hào)必須和用戶(hù)賬號(hào)完全相同檐晕;通過(guò)用戶(hù)名搜索時(shí)是模糊查詢(xún),有一部分匹配即可蚌讼。搜索到用戶(hù)后即可向其發(fā)送好友申請(qǐng)等待通過(guò)辟灰。
用戶(hù)可以在“好友申請(qǐng)”界面查看其他用戶(hù)發(fā)來(lái)的申請(qǐng),如果點(diǎn)擊“同意”篡石,則兩位用戶(hù)之間建立好友關(guān)系芥喇,可以互相之間查看動(dòng)態(tài)。好友關(guān)系通過(guò)用戶(hù)關(guān)系表來(lái)保存凰萨,如果ID為1的用戶(hù)和ID為2的用戶(hù)是好友關(guān)系继控,那么表中會(huì)存儲(chǔ)(1, 2)和(2, 1)這兩組數(shù)據(jù)械馆。
4. 發(fā)送動(dòng)態(tài)
在Android客戶(hù)端上傳動(dòng)態(tài)時(shí),將用戶(hù)輸入的文字作為一個(gè)參數(shù)湿诊,將選擇的每張圖片各作為一個(gè)參數(shù)再調(diào)用接口狱杰。
在服務(wù)器端瘦材,接口循環(huán)接收參數(shù)并判斷當(dāng)前上傳的參數(shù)是文字還是文件厅须,如果是文字則保存到動(dòng)態(tài)表中的文字字段;如果是文件食棕,則按順序保存到指定目錄下并在表中保存圖片的路徑或名字朗和。
5. 時(shí)間線(xiàn)
那么一個(gè)用戶(hù)發(fā)送的動(dòng)態(tài)怎么顯示在他好友的動(dòng)態(tài)列表中呢?這里就需要時(shí)間線(xiàn)這張表的幫助簿晓。當(dāng)某位用戶(hù)發(fā)表動(dòng)態(tài)的時(shí)候眶拉,系統(tǒng)會(huì)先將該動(dòng)態(tài)的文字和圖片保存,得到該動(dòng)態(tài)的主鍵ID憔儿。再搜索該用戶(hù)的所有好友忆植,對(duì)每個(gè)好友,以(好友ID, 動(dòng)態(tài)ID)的形式在時(shí)間線(xiàn)表中添加一條記錄谒臼。
當(dāng)一個(gè)用戶(hù)在首頁(yè)請(qǐng)求好友的動(dòng)態(tài)列表時(shí)朝刊,后臺(tái)會(huì)在時(shí)間線(xiàn)表中搜索“好友ID”字段等于該用戶(hù)ID的記錄,拉取最近的N條記錄并倒序返回蜈缤。
6. 評(píng)論管理
本應(yīng)用的評(píng)論系統(tǒng)較為簡(jiǎn)單拾氓,用戶(hù)只能對(duì)動(dòng)態(tài)進(jìn)行評(píng)論,無(wú)法回復(fù)在動(dòng)態(tài)下評(píng)論的其他用戶(hù)底哥。當(dāng)然咙鞍,如果要實(shí)現(xiàn)回復(fù)功能,只需要在每條評(píng)論上再加個(gè)字段表明該評(píng)論是回復(fù)哪條評(píng)論的即可趾徽。
7. 點(diǎn)贊管理
由于每個(gè)用戶(hù)只能為某條動(dòng)態(tài)點(diǎn)贊一次续滋,因此需要一個(gè)表來(lái)保存動(dòng)態(tài)的點(diǎn)贊情況。這里為了簡(jiǎn)化業(yè)務(wù)孵奶,將點(diǎn)贊設(shè)置為無(wú)法取消疲酌。當(dāng)用戶(hù)點(diǎn)贊某條動(dòng)態(tài)時(shí),動(dòng)態(tài)表中對(duì)應(yīng)動(dòng)態(tài)的點(diǎn)贊數(shù)量字段+1拒课,并在點(diǎn)贊表中保存(動(dòng)態(tài)ID, 用戶(hù)ID)形式的記錄徐勃。用戶(hù)查看某條動(dòng)態(tài)時(shí),如果(動(dòng)態(tài)ID, 用戶(hù)ID)記錄存在早像,則將點(diǎn)贊圖標(biāo)設(shè)置為紅色僻肖。
雖然本應(yīng)用采用了上述的方式管理點(diǎn)贊,但是很容易產(chǎn)生大量記錄降低運(yùn)行效率卢鹦。一種解決辦法是使用非關(guān)系型數(shù)據(jù)庫(kù)例如redis臀脏,通過(guò)鍵值對(duì)來(lái)存儲(chǔ)為某條動(dòng)態(tài)點(diǎn)贊的所有用戶(hù)劝堪。如果堅(jiān)持用關(guān)系型數(shù)據(jù)庫(kù)處理點(diǎn)贊,可以使用類(lèi)似(動(dòng)態(tài)ID, [用戶(hù)1, 用戶(hù)2, 用戶(hù)3...])的方式處理揉稚。
四秒啦、數(shù)據(jù)庫(kù)設(shè)計(jì)
1. 用戶(hù)表user
用戶(hù)表需要記錄用戶(hù)的登陸憑證(賬號(hào)、密碼)以及用戶(hù)的個(gè)人信息搀玖,包括用戶(hù)名余境、頭像、背景和簽名等灌诅。
字段名 | 數(shù)據(jù)類(lèi)型 | 長(zhǎng)度 | 約束 | 描述 |
---|---|---|---|---|
id | int | 11 | 主鍵, 自增 | 用戶(hù)ID |
account | varchar | 20 | 主鍵 | 賬號(hào) |
password | varchar | 30 | 非空 | 密碼 |
username | varchar | 30 | 用戶(hù)名 | |
icon | varchar | 100 | 頭像文件的名字 | |
background | varchar | 100 | 背景圖片的名字 | |
signature | varchar | 60 | 個(gè)性簽名 |
2. 好友關(guān)系表user_link
字段名 | 數(shù)據(jù)類(lèi)型 | 長(zhǎng)度 | 約束 | 描述 |
---|---|---|---|---|
id | int | 11 | 主鍵, 自增 | 唯一ID |
userid | int | 11 | 主鍵 | 用戶(hù)ID |
linkid | int | 11 | 非空 | 好友ID |
remark | varchar | 20 | 備注名 |
3. 好友申請(qǐng)表friendrequest
用戶(hù)的好友請(qǐng)求頁(yè)面會(huì)顯示該用戶(hù)收到的請(qǐng)求芳来,由于請(qǐng)求分為“未同意”和“已同意”兩種,因此表中需要一個(gè)status字段標(biāo)注當(dāng)前請(qǐng)求的狀態(tài)猜拾,0表示未同意即舌,1表示已同意。如果用戶(hù)同意了某個(gè)好友請(qǐng)求挎袜,那么該請(qǐng)求的狀態(tài)就會(huì)從0變?yōu)?顽聂,同時(shí)兩人建立好友關(guān)系。
字段名 | 數(shù)據(jù)類(lèi)型 | 長(zhǎng)度 | 約束 | 描述 |
---|---|---|---|---|
id | int | 11 | 主鍵, 自增 | 唯一ID |
sender | int | 11 | 非空 | 發(fā)送請(qǐng)求的用戶(hù)ID |
receiver | int | 11 | 非空 | 接到請(qǐng)求的用戶(hù)ID |
status | int | 1 | 該請(qǐng)求的狀態(tài) |
4. 動(dòng)態(tài)詳情表moments_message
字段名 | 數(shù)據(jù)類(lèi)型 | 長(zhǎng)度 | 約束 | 描述 |
---|---|---|---|---|
id | int | 11 | 主鍵, 自增 | 唯一ID |
userid | int | 11 | 非空 | 用戶(hù)ID |
content | varchar | 200 | 非空 | 動(dòng)態(tài)的文字內(nèi)容 |
picture1 | varchar | 100 | 圖片1 | |
picture2 | varchar | 100 | 圖片2 | |
picture3 | varchar | 100 | 圖片3 | |
picture4 | varchar | 100 | 圖片4 | |
picture5 | varchar | 100 | 圖片5 | |
picture6 | varchar | 100 | 圖片6 | |
createtime | datetime | 創(chuàng)建時(shí)間 | ||
likes | int | 11 | 點(diǎn)贊數(shù)量(默認(rèn)0) | |
comments | int | 11 | 評(píng)論數(shù)量(默認(rèn)0) |
5. 時(shí)間線(xiàn)表timeline
字段名 | 數(shù)據(jù)類(lèi)型 | 長(zhǎng)度 | 約束 | 描述 |
---|---|---|---|---|
id | int | 11 | 主鍵, 自增 | 唯一ID |
userid | int | 11 | 非空 | 用戶(hù)ID |
momentid | int | 11 | 非空 | 動(dòng)態(tài)ID |
6. 評(píng)論表moments_comment
字段名 | 數(shù)據(jù)類(lèi)型 | 長(zhǎng)度 | 約束 | 描述 |
---|---|---|---|---|
id | int | 11 | 主鍵, 自增 | 唯一ID |
momentid | int | 11 | 非空 | 動(dòng)態(tài)ID |
userid | int | 11 | 非空 | 用戶(hù)ID |
content | varchar | 100 | 非空 | 評(píng)論內(nèi)容 |
7. 點(diǎn)贊表moments_like
字段名 | 數(shù)據(jù)類(lèi)型 | 長(zhǎng)度 | 約束 | 描述 |
---|---|---|---|---|
id | int | 11 | 主鍵, 自增 | 唯一ID |
momentid | int | 11 | 非空 | 動(dòng)態(tài)ID |
userid | int | 11 | 非空 | 用戶(hù)ID |
五盯仪、后臺(tái)的部分實(shí)現(xiàn)
本應(yīng)用后臺(tái)的實(shí)現(xiàn)比較基礎(chǔ)紊搪,主要是兩個(gè)方面,一是通過(guò)數(shù)據(jù)庫(kù)連接池和DAO實(shí)現(xiàn)數(shù)據(jù)的增刪改查磨总;二是通過(guò)Servlet實(shí)現(xiàn)數(shù)據(jù)的上傳下載嗦明。這里挑選一部分介紹,其他大同小異蚪燕。
1. 數(shù)據(jù)庫(kù)連接池
本應(yīng)用使用的數(shù)據(jù)庫(kù)連接池是druid連接池娶牌,由于數(shù)據(jù)庫(kù)連接池本身依賴(lài)于jdbc的驅(qū)動(dòng),所以需要在項(xiàng)目的WebContent/WEB-INF/lib下添加mysql-connector-java的包馆纳,為了支持uft8mb4編碼诗良,建議使用高版本的jar包(這里使用的版本是5.1.34),然后添加druid連接池的jar包鲁驶,不要忘了對(duì)jar包執(zhí)行add to build path操作鉴裹。數(shù)據(jù)庫(kù)連接池一般通過(guò)文件進(jìn)行配置,在項(xiàng)目的src文件下添加配置文件druid.properties钥弯,如下所示径荔。
第一行聲明了數(shù)據(jù)庫(kù)連接的驅(qū)動(dòng),最后一行聲明了連接的編碼為utf8mb4脆霎,其余url总处、username和password等信息注意修改。
driverClassName=com.mysql.jdbc.Driver
url=jdbc\:mysql\://localhost\:3306/moments
username=root
password=root
filters=stat
initialSize=2
minIdle=1
maxActive=300
maxWait=60000
timeBetweenEvictionRunsMillis=60000
minEvictableIdleTimeMillis=300000
validationQuery=SELECT 'x'
testWhileIdle=true
testOnBorrow=false
testOnReturn=false
poolPreparedStatements=false
maxPoolPreparedStatementPerConnectionSize=20
connectionInitSqls=[set names utf8mb4]
添加完配置文件之后睛蛛,需要一個(gè)類(lèi)對(duì)數(shù)據(jù)庫(kù)連接池的基本操作進(jìn)行封裝鹦马。新建DbPoolConnection類(lèi)胧谈,該類(lèi)使用單例模式,保證在程序的運(yùn)行過(guò)程中只存在一個(gè)DbPoolConnection實(shí)例荸频。類(lèi)中通過(guò)靜態(tài)方法loadPropertyFile()來(lái)加載druid.properties文件中的配置菱肖,通過(guò)getConnection()方法獲取一個(gè)連接,通過(guò)releaseConnection(Connection connection)方法釋放一個(gè)連接旭从。
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.alibaba.druid.pool.DruidPooledConnection;
public class DbPoolConnection {
private static DbPoolConnection databasePool = null;
private static DruidDataSource dds = null;
static {
Properties properties = loadPropertyFile("druid.properties");
try {
dds = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
private DbPoolConnection() { }
public static synchronized DbPoolConnection getInstance() {
if (null == databasePool) {
databasePool = new DbPoolConnection();
}
return databasePool;
}
public static DruidPooledConnection getConnection() throws SQLException {
return dds.getConnection();
}
public static void releaseConnection(Connection connection) throws SQLException {
connection.close();
}
public static Properties loadPropertyFile(String fullFile) {
String webRootPath = null;
if (null == fullFile || fullFile.equals(""))
throw new IllegalArgumentException("Properties file path can not be null: " + fullFile);
webRootPath = DbPoolConnection.class.getClassLoader().getResource("").getPath();
InputStream inputStream = null;
Properties p = null;
try {
String sglPath = webRootPath + File.separator + fullFile;
sglPath = URLDecoder.decode(sglPath, "utf-8"); // 關(guān)鍵
inputStream = new FileInputStream(new File(sglPath));
p = new Properties();
p.load(inputStream);
} catch (FileNotFoundException e) {
throw new IllegalArgumentException("Properties file not found: " + fullFile);
} catch (IOException e) {
throw new IllegalArgumentException("Properties file can not be loading: " + fullFile);
} finally {
try {
if (inputStream != null)
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return p;
}
}
2. 數(shù)據(jù)的增刪改查
這里定義一個(gè)泛型類(lèi)DAO<T>并使用QueryRunner 封裝數(shù)據(jù)庫(kù)增刪改查的基本操作稳强,這里的T可以代表任何數(shù)據(jù)實(shí)體,如果需要對(duì)某個(gè)實(shí)體進(jìn)行操作遇绞,定義一個(gè)繼承自DAO<T>的類(lèi)即可褐健。DAO<T>如下所示:
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import com.lister.db.DbPoolConnection;
public class DAO<T> {
private QueryRunner queryRunner = new QueryRunner();
private Class<T> clazz;
@SuppressWarnings("unchecked")
public DAO() {
// 獲得帶有泛型的父類(lèi)
Type superClass = getClass().getGenericSuperclass();
// ParameterizedType 是參數(shù)化類(lèi)型刹帕,即泛型
if (superClass instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) superClass;
// 因?yàn)榉盒涂赡苡卸鄠€(gè)侦另,所以使用參數(shù)類(lèi)型數(shù)組保存
Type[] typeArgs = parameterizedType.getActualTypeArguments();
if (typeArgs != null && typeArgs.length > 0) {
if (typeArgs[0] instanceof Class) {
clazz = (Class<T>) typeArgs[0];
}
}
}
}
/**
* 獲取到數(shù)據(jù)庫(kù)中的某個(gè)字段的值
* @throws SQLException
*/
@SuppressWarnings("unchecked")
public <E> E getValue(String sql, Object ... args) throws SQLException {
Connection connection = null;
try {
connection = DbPoolConnection.getConnection();
return (E) queryRunner.query(connection, sql, new ScalarHandler(), args);
} catch (Exception e) {
e.printStackTrace();
} finally {
DbPoolConnection.releaseConnection(connection);
}
return null;
}
/**
* 獲取到數(shù)據(jù)庫(kù)中的 N 條記錄的列表
* @throws SQLException
*/
public List<T> getList(String sql, Object ... args) throws SQLException {
Connection connection = null;
try {
connection = DbPoolConnection.getConnection();
return queryRunner.query(connection, sql, new BeanListHandler<>(clazz), args);
} catch (Exception e) {
e.printStackTrace();
} finally {
DbPoolConnection.releaseConnection(connection);
}
return null;
}
/**
* 獲取到數(shù)據(jù)庫(kù)中的一條記錄
* @throws SQLException
*/
public T get(String sql, Object ... args) throws SQLException {
Connection connection = null;
try {
connection = DbPoolConnection.getConnection();
return queryRunner.query(connection, sql, new BeanHandler<>(clazz), args);
} catch (Exception e) {
e.printStackTrace();
} finally {
DbPoolConnection.releaseConnection(connection);
}
return null;
}
/**
* 進(jìn)行 insert, delete, update 操作
* @throws SQLException
*/
public void update(String sql, Object ... args) throws SQLException {
Connection connection = null;
try {
connection = DbPoolConnection.getConnection();
queryRunner.update(connection, sql, args);
} catch (Exception e) {
e.printStackTrace();
} finally {
DbPoolConnection.releaseConnection(connection);
}
}
}
有了DAO這個(gè)基類(lèi)之后柳洋,就可以通過(guò)繼承自它的類(lèi)來(lái)對(duì)指定的表的數(shù)據(jù)進(jìn)行操作莹捡。
舉個(gè)例子,假如要對(duì)moments_comment表進(jìn)行操作,首先定義實(shí)體類(lèi)Comment芒涡,實(shí)體類(lèi)中的字段需要與數(shù)據(jù)庫(kù)表內(nèi)的字段一一對(duì)應(yīng)卖漫。
public class Comment {
Integer id;
Integer momentid;
Integer userid;
String content;
// getters and setters
}
那么對(duì)評(píng)論表有哪些操作呢费尽?無(wú)非就是兩個(gè):1. 為某條動(dòng)態(tài)下添加一條評(píng)論; 2. 獲取某條動(dòng)態(tài)下的所有評(píng)論。根據(jù)對(duì)表的操作寫(xiě)出如下接口羊始。
import java.util.List;
import com.lister.model.Comment;
public interface CommentDAO {
// 為某條動(dòng)態(tài)添加評(píng)論
public void addComment(Integer momentid, Integer userid, String content) throws Exception;
//獲取某條動(dòng)態(tài)所有評(píng)論
public List<Comment> getComments(Integer id) throws Exception;
}
有了實(shí)體類(lèi)和接口之后旱幼,可以編寫(xiě)具體的操作來(lái)實(shí)現(xiàn)接口了。定義一個(gè)繼承自DAO<Comment>并實(shí)現(xiàn)了CommentDAO接口的類(lèi)突委。方法中的?表示占位符柏卤,在最后調(diào)用DAO基類(lèi)的方法時(shí)用參數(shù)填充SQL語(yǔ)句里的占位符冬三。
import java.util.List;
import com.lister.dao.CommentDAO;
import com.lister.dao.DAO;
import com.lister.model.Comment;
public class CommentDAOImpl extends DAO<Comment> implements CommentDAO {
@Override
public void addComment(Integer momentid, Integer userid, String content) throws Exception {
String sql = "insert into moments_comment values(0, ?, ?, ?)";
update(sql, momentid, userid, content);
}
@Override
public List<Comment> getComments(Integer momentid) throws Exception {
String sql = "select * from moments_comment where momentid = ? order by id desc";
return getList(sql, momentid);
}
}
之后即可通過(guò)CommentDAOImpl的實(shí)例來(lái)操作評(píng)論表。對(duì)于其他表的操作缘缚,也可以使用類(lèi)似的方式勾笆。
3. 文件的上傳
應(yīng)用中的頭像修改和發(fā)表動(dòng)態(tài)都涉及到圖片文件的上傳,這里以頭像修改為例介紹文件上傳的方式桥滨。
為了支持文件上傳窝爪,需要添加jar包,文件上傳依賴(lài)common-fileupload和common-io這兩個(gè)包齐媒。而在服務(wù)器端接收文件蒲每,與平時(shí)的接收參數(shù)有一點(diǎn)差別帅刀,如果只有文字參數(shù)矢劲,一般通過(guò)參數(shù)名來(lái)獲取參數(shù)值;如果帶有文件參數(shù)他膳,則通過(guò)一個(gè)循環(huán)接收參數(shù)并判斷該參數(shù)是普通參數(shù)(文字)還是文件參數(shù)双妨,再進(jìn)行對(duì)應(yīng)的操作淮阐。服務(wù)器端頭像修改的代碼如下,已通過(guò)注釋詳細(xì)介紹刁品。
/**
* 接口:處理 changeicon.user 請(qǐng)求 修改頭像
* @throws Exception
*/
public void changeicon(HttpServletRequest request, HttpServletResponse response)
throws Exception {
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
/**
* 由于上傳頭像時(shí)包含文件, 使用 FileItem 獲取參數(shù)
*/
String idString = "0";
String account = "";
String picpath = "";
try {
// 得到上傳文件的保存目錄
String realPath = this.getServletContext().getRealPath("/upload");
String tempPath = "C:\\tempPath"; // 臨時(shí)目錄
// 判斷存放上傳文件的目錄是否存在
File f = new File(realPath);
if (!f.exists() && !f.isDirectory()) {
f.mkdir();
}
// 判斷臨時(shí)目錄是否存在
File tempFilePath = new File(tempPath);
if (!tempFilePath.isDirectory()) {
tempFilePath.mkdir();
}
/**
* 使用Apache文件上傳組件處理文件上傳步驟
*/
// 1. 設(shè)置環(huán)境:創(chuàng)建一個(gè)DiskFileItemFactory工廠
DiskFileItemFactory factory = new DiskFileItemFactory();
// 設(shè)置上傳文件的臨時(shí)目錄
factory.setRepository(tempFilePath);
// 2. 核心操作類(lèi): 創(chuàng)建一個(gè)文件上傳解析器。
ServletFileUpload upload = new ServletFileUpload(factory);
// 解決上傳"文件名"的中文亂碼
upload.setHeaderEncoding("UTF-8");
// 3. 判斷 enctype:判斷提交上來(lái)的數(shù)據(jù)是否是上傳表單的數(shù)據(jù)
if (!ServletFileUpload.isMultipartContent(request)) {
System.out.println("不是上傳文件浩姥,終止");
// 按照傳統(tǒng)方式獲取數(shù)據(jù)
return;
}
// 4. 使用ServletFileUpload解析器解析上傳數(shù)據(jù)挑随,
// 解析結(jié)果返回的是一個(gè)List<FileItem>集合
// 每一個(gè)FileItem對(duì)應(yīng)一個(gè)Form表單的輸入項(xiàng)
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
// 如果 fileItem 中封裝的是普通輸入項(xiàng)的數(shù)據(jù)
if (item.isFormField()) {
String filedName = item.getFieldName();// 普通輸入項(xiàng)數(shù)據(jù)的名
// 解決普通輸入項(xiàng)的數(shù)據(jù)的中文亂碼問(wèn)題
String filedValue = item.getString("UTF-8");
if (filedName.equals("account")) {
account = filedValue;
} else if (filedName.equals("id")) {
idString = filedValue;
}
} else {
// 如果 fileItem 中封裝的是上傳文件, 得到上傳的文件名稱(chēng)
String fileName = item.getName();
// 多個(gè)文件上傳輸入框有空 的 異常處理
if (fileName == null || "".equals(fileName.trim())) {
continue;
}
// 處理上傳文件的文件名的路徑, 截取字符串只保留文件名部分
// 截取留最后一個(gè)"\"之后,+1 截取向右移一位 "\a.txt"-->"a.txt"
fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
String suffix = fileName.substring(fileName.lastIndexOf("."));
// 使用當(dāng)前時(shí)間作為新的文件名
SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
fileName = account + df.format(new Date()) + suffix;
// 拼接上傳路徑: 存放路徑 + 上傳的文件名
String filePath = realPath + "\\" + fileName;
// 構(gòu)建輸入輸出流
InputStream in = item.getInputStream();
OutputStream out = new FileOutputStream(filePath);
byte b[] = new byte[1024];
int len = -1;
while ((len = in.read(b)) != -1) {
out.write(b, 0, len);
}
out.close(); in.close();
// 刪除臨時(shí)文件
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
item.delete();
// picpath 賦值
picpath = fileName;
}
}
// 循環(huán)完畢, 添加記錄
Integer id = Integer.parseInt(idString);
if (id > 0 && !account.trim().equals("")) {
if (userDAO.isUserExist(id, account) != 0) {
userDAO.changeUserIcon(id, picpath);
RevertMessage revertMessage = new RevertMessage(true, "頭像上傳成功");
writer.print(gson.toJson(revertMessage));
} else {
// 用戶(hù)不存在
RevertMessage revertMessage = new RevertMessage(false, "用戶(hù)不存在");
writer.print(gson.toJson(revertMessage));
}
} else {
// 賬號(hào)為空
RevertMessage revertMessage = new RevertMessage(false, "賬號(hào)為空");
writer.print(gson.toJson(revertMessage));
}
} catch (FileUploadException e) {
// 上傳異常
System.out.println(e.getMessage());
RevertMessage revertMessage = new RevertMessage(false, "文件上傳異常");
writer.print(gson.toJson(revertMessage));
}
}
六勒叠、Android的部分實(shí)現(xiàn)
1. 首頁(yè)布局
用戶(hù)進(jìn)入應(yīng)用后兜挨,屏幕下方的選項(xiàng)卡默認(rèn)為“動(dòng)態(tài)”,上方顯示用戶(hù)自定義的背景和頭像眯分,點(diǎn)擊右上角的圖案可發(fā)送動(dòng)態(tài)拌汇。當(dāng)用戶(hù)在首頁(yè)向下滑動(dòng)查看好友的動(dòng)態(tài)時(shí),上方的背景圖會(huì)逐漸消失弊决,具體效果可見(jiàn)第一張圖噪舀。那么怎么實(shí)現(xiàn)呢?
Android的CoordinatorLayout就可以實(shí)現(xiàn)這類(lèi)效果飘诗。在布局時(shí)与倡,使用CoordinatorLayout布局包裹CollapsingToolbarLayout,從CollapsingToolbarLayout這個(gè)布局的名字我們可以看出一些端倪昆稿。Collapse什么意思纺座?坍塌,折疊溉潭。因此這是一個(gè)會(huì)折疊的布局净响,只要對(duì)它的行為做一些設(shè)置少欺,就可以實(shí)現(xiàn)滑動(dòng)后折疊的效果。因此可以在CollapsingToolbarLayout中放置背景圖馋贤、用戶(hù)名和頭像等控件赞别。布局效果如下:
布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.lister.momentsandroid.activity.MomentsActivity">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/moments_appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/moments_collapse_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:collapsedTitleTextAppearance="@style/ToolBarTitleText"
app:contentScrim="@color/mainPurple"
app:expandedTitleMarginEnd="48dp"
app:expandedTitleMarginStart="48dp"
app:expandedTitleTextAppearance="@style/transparentText"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:fitsSystemWindows="true">
<LinearLayout
android:id="@+id/moments_info_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_collapseMode="pin"
app:layout_collapseParallaxMultiplier="0.7">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="240dp"
android:background="@color/light_gray">
<ImageView
android:id="@+id/moments_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
<LinearLayout
android:id="@+id/moments_post"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="30dp"
android:layout_marginLeft="30dp"
android:src="@drawable/moments_post_icon"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="15dp"
android:layout_marginRight="20dp"
android:orientation="vertical">
<TextView
android:id="@+id/moments_text_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/white"
android:layout_marginBottom="8dp"/>
<ImageView
android:id="@+id/moments_icon"
android:layout_width="80dp"
android:layout_height="80dp"
android:background="@color/white"
android:padding="2dp"/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="45dp"
android:clickable="true"
app:layout_collapseMode="pin"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:titleTextAppearance="@style/Toolbar.TitleText"/>
</android.support.design.widget.CollapsingToolbarLayout>
<android.support.design.widget.TabLayout
android:id="@+id/moments_tab_layout"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="bottom"
android:background="#ffffff"
android:fillViewport="false"
app:layout_scrollFlags="scroll"
app:tabIndicatorColor="@color/mainPurple"
app:tabIndicatorHeight="2dp"
app:tabSelectedTextColor="@color/mainPurple"
app:tabTextColor="#ced0d3">
<android.support.design.widget.TabItem
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="圈內(nèi)動(dòng)態(tài)"/>
<android.support.design.widget.TabItem
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="公開(kāi)動(dòng)態(tài)"/>
</android.support.design.widget.TabLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/moments_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>
</LinearLayout>
布局文件中用到了兩個(gè)樣式:
<style name="ToolBarTitleText" parent="TextAppearance.AppCompat.Medium">
<item name="android:textColor">#ffffffff</item>
<item name="android:textSize">16sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="transparentText" parent="TextAppearance.AppCompat.Small">
<item name="android:textColor">#00000000</item>
</style>
在Activity文件中,需要對(duì)ToolBar和AppBarLayout進(jìn)行設(shè)置掸掸。在onOffsetChanged方法中監(jiān)聽(tīng)折疊的程度并逐漸改變ToolBar的顏色氯庆,當(dāng)背景圖區(qū)域折疊超過(guò)一半的時(shí)候,屏幕上方的ToolBar顯示“動(dòng)態(tài)”扰付,否則不顯示文字堤撵。
@BindView(R.id.toolbar) Toolbar mToolbar;
@BindView(R.id.moments_collapse_layout) CollapsingToolbarLayout mCollapsingToolbarLayout;
@BindView(R.id.moments_appbar_layout) AppBarLayout mAppBarLayout;
@BindView(R.id.moments_info_linear) LinearLayout mInfoLinear;
// ......
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_moments);
ButterKnife.bind(this);
// 注冊(cè) EventBus
EventBus.getDefault().register(this);
// 設(shè)置 ToolBar
setSupportActionBar(mToolbar);
// AppBarLayout
mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
// 逐漸修改顏色
mToolbar.setBackgroundColor(changeAlpha(getResources().getColor(R.color.mainPurple),
Math.abs(verticalOffset * 1.0f) / appBarLayout.getTotalScrollRange()));
if (verticalOffset <= -mInfoLinear.getHeight() / 2) {
mCollapsingToolbarLayout.setTitle("動(dòng)態(tài)");
} else {
mCollapsingToolbarLayout.setTitle("");
}
}
});
// ......
}
2. 聯(lián)網(wǎng)操作封裝
本應(yīng)用使用OKHTTP框架進(jìn)行聯(lián)網(wǎng)操作,新建HttpUtils類(lèi)封裝http請(qǐng)求羽莺。首先在類(lèi)中定義一些私有變量來(lái)做緩存控制实昨,如下所示:
private static final CacheControl FORCE_NETWORK = new CacheControl.Builder().noCache().build();
private static final CacheControl FORCE_CACHE = new CacheControl.Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
// 緩存控制
public static final String TYPE_FORCE_CACHE = "TYPE_FORCE_CACHE";
public static final String TYPE_FORCE_NETWORK = "TYPE_FORCE_NETWORK";
public static final String TYPE_CACHE_CONTROL = "TYPE_CACHE_CONTROL";
來(lái)看最基礎(chǔ)的post請(qǐng)求,在上傳的參數(shù)只有文字的情況下調(diào)用此方法盐固。
public static Object[] postHttp(Context context, String url, HashMap<String, String> params, String cacheType, int cacheSeconds) {
try {
// 緩存文件夾
File cacheFile = new File(context.getExternalCacheDir().toString(), "cache");
// 緩存大小為50M
int cacheSize = 50 * 1024 * 1024;
// 創(chuàng)建緩存對(duì)象
final Cache cache = new Cache(cacheFile, cacheSize);
OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.cache(cache)
.build();
FormBody.Builder formBodyBuilder = new FormBody.Builder();
for (Map.Entry<String, String> entry : params.entrySet())
formBodyBuilder.add(entry.getKey(), entry.getValue());
RequestBody formBody = formBodyBuilder.build();
CacheControl cacheControl = null;
if (cacheType.equals(TYPE_CACHE_CONTROL)) {
cacheControl = new CacheControl.Builder()
.maxAge(cacheSeconds, TimeUnit.SECONDS).build();
}
if (cacheType.equals(TYPE_FORCE_CACHE)) {
cacheControl = FORCE_CACHE;
}
if (cacheType.equals(TYPE_FORCE_NETWORK)) {
cacheControl = FORCE_NETWORK;
}
Request request = new Request.Builder()
.cacheControl(cacheControl)
.url(url)
.post(formBody)
.build();
Response response = mOkHttpClient.newCall(request).execute();
String result = response.body().string();
// 處理result并return
} catch (Exception e) {
// ......
}
}
調(diào)用示例:
// 參數(shù)分別為 Context, 接口地址, HashMap<String, String>參數(shù), 緩存控制, 緩存時(shí)間
// 這里的TYPE_FORCE_NETWORK指不用緩存, 強(qiáng)制聯(lián)網(wǎng)獲取資源
return HttpUtils.postHttp(ChangeNameActivity.this,
IPConstant.CHANGE_USER_NAME, params,
HttpUtils.TYPE_FORCE_NETWORK, 0);
那么如果上傳的參數(shù)里包含圖片呢荒给?那么就需要兩類(lèi)參數(shù),也就是兩個(gè)Map來(lái)保存參數(shù)刁卜,一個(gè)保存文字志电,一個(gè)保存文件。
public static Object[] postHttpJPGLinked(
Context context, String url, LinkedHashMap<String, String> params, LinkedHashMap<String, File> pictures) {
try {
OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build();
MultipartBody.Builder multipartBody = new MultipartBody.Builder();
multipartBody.setType(MultipartBody.FORM);
for (Map.Entry<String, String> entry : params.entrySet())
multipartBody.addFormDataPart(entry.getKey(), entry.getValue());
for (Map.Entry<String, File> entry : pictures.entrySet()) {
RequestBody fileBody = RequestBody.create(MediaType.parse("image/jpg"), entry.getValue());
multipartBody.addFormDataPart(entry.getKey(), entry.getKey() + ".jpg", fileBody);
}
RequestBody requestBody = multipartBody.build();
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Response response = mOkHttpClient.newCall(request).execute();
String result = response.body().string();
// ......
} catch (Exception e) {
// ......
}
}
3. 發(fā)送動(dòng)態(tài)在相冊(cè)選取圖片
從相冊(cè)選取圖片主要是參考下方的第10篇博客蛔趴,之后將圖片添加到一個(gè)List里方便上傳挑辆。由于鴻洋大神的博客已經(jīng)講得很詳細(xì)了,我就不多說(shuō)了孝情。
總結(jié)
做這個(gè)應(yīng)用的時(shí)候趕著秋招鱼蝉,一直剩下幾個(gè)坑沒(méi)填,例如上傳圖片前沒(méi)有壓縮箫荡、密碼沒(méi)有加密和沒(méi)有使用token等······但是做應(yīng)用的過(guò)程中魁亦,我更多思考的是朋友圈如何實(shí)現(xiàn)組群權(quán)限以及高并發(fā)。如果有朋友了解組群權(quán)限和集群這方面內(nèi)容的羔挡,希望能分享一下洁奈,拜謝~
另:最近又有個(gè)新的項(xiàng)目構(gòu)思,是做一個(gè)主要功能為視頻上傳和觀看的應(yīng)用婉弹,這次有充分的時(shí)間將之前沒(méi)完善的方面全部做好睬魂,完成以后依舊會(huì)以博客的形式分享。
參考:
- 微信朋友圈架構(gòu):http://www.reibang.com/p/3fb3652ff450
- 數(shù)據(jù)庫(kù)設(shè)計(jì):https://www.zhihu.com/question/21909660
- MySQL修改編碼為utf8mb4:https://www.cnblogs.com/shihaiming/p/5855616.html
- 將表字段編碼修改為utf8mb4:https://blog.csdn.net/luo4105/article/details/50804148
- 添加服務(wù)器包:https://blog.csdn.net/evan_leung/article/details/50647112
- Servlet注解:https://blog.csdn.net/mytt_10566/article/details/71077154
- 文件上傳:https://www.cnblogs.com/liuyangv/p/8298997.html
- Tomcat部署項(xiàng)目:https://www.cnblogs.com/ysocean/p/6893446.html
- CoordinatorLayout打造詳情頁(yè):http://www.reibang.com/p/5287d090e777
- 仿微信圖片選擇器:https://blog.csdn.net/lmj623565791/article/details/39943731/
- OKHTTP:https://blog.csdn.net/lmj623565791/article/details/47911083