編輯:關於Android編程
移動端集成支付,似乎是每個App都可能面臨的一件事。所有項目都在談盈利模式,而從C端獲取現金流是盈利中最重要的一個途徑之一。
當前大家主要采用微信支付和阿裡支付集成到自己的移動應用,雖然官方提供一些文檔和Demo,但是文檔的不完整性,信息分散,集成的時候,依然會有很多困惑。本文從Android的Client集成,到服務器集成對阿裡支付集成做介紹。
從網上找到的文章裡,大多數只說了如何集成客戶端,但是很少提及服務器。雖然如此,服務器集成是一定要做的。如果僅憑客服端返的數據就更改訂單狀態是極為危險的,因為任何人都可以通過技術手段偽造數據,發數據給服務器修改訂單狀態,會造成極大的經濟損失。並且強烈建議,訂單狀態的修改不要依賴客戶端,客戶端的返回,可以作為校驗輔助。
下面是集成阿裡支付的數據交互流,可以在這裡找到:
https://doc.open.alipay.com/doc2/detail?treeId=59&articleId=103563&docType=1
用戶在客戶端發起支付後,打開支付寶。需要解釋的一點是,無論是完成支付,還是按掉叉叉取消支付,阿裡支付的服務器端都會給應用服務器(商戶服務器,“我們的服務器”)同步一條數據,通知服務器用戶的詳細支付結果。所以客戶端集成和服務器集成都必不可少。
本文分別從Android客戶端和Java服務器端集成,介紹集成阿裡支付的詳細過程。
其中,Android端支付,主要參考了官方Demo和博文:
http://my.oschina.net/daniels/blog/597356?fromerr=CxXSKO5S
服務器端集成主要是在官方Demo基礎上自己實踐,服務器端可運行代碼在我的Github上
https://github.com/hopeztm7500/AlipayServerDemo
1. RSA算法概述
如果不先說點關於數據校驗到事情,肯定對配置支付環境出現的各種密鑰十分困惑。在數據流圖中,我們發現有兩次跨應用的數據交互,前端我們吊起了支付寶,而支付寶發送同步數據給應用服務器。特別是服務器同步數據,原理再簡單不過,就是一個POST請求,用Shell嚴格仿照格式任何程序員都能構造出來,那麼應用服務器如何辨別這條數據是來自支付寶服務器而不是偽造的呢。這裡就涉及到數據簽名校驗。
簡單可以理解RSA校驗按照如下到方式工作:
(1)私鑰用來進行解密和簽名,是給自己用的。
(2)公鑰由本人公開,用於加密和驗證簽名,是給別人用的。
(3)當該用戶發送文件時,用私鑰簽名,別人用他給的公鑰驗證簽名,可以保證該信息是由他發送的。當該用戶接受文件時,別人用他的公鑰加密,他用私鑰解密,可以保證該信息只能由他接收到。
所以在兩處數據交互的地方,我們都用到了RSA算法,在前端調用支付寶的時候,我們自己生成一對公鑰和私鑰(商戶公鑰和私鑰),公鑰我們要填寫到支付寶後台,而私鑰要自己寫在客戶端程序中。這裡我推薦把私鑰存儲在SO文件中,防止被反編譯獲取利用。
而服務器交互數據的時候,支付寶服務器用自己持有的私鑰對數據做簽名,應用服務器使用支付寶提供的公鑰對數據做校驗。所以在集成中,你會用在應用客戶端使用商戶私鑰,在支付寶後台配置商戶公鑰,在應用服務器使用支付寶公鑰。
2. Android 客戶端集成
目錄[-]
首先登錄【支付寶開放平台】http://open.alipay.com/platform/home.htm,添加應用,申請移動支付權限。申請開通支付,是需要公司文件的,個人是不允許開始支付的。
具體細節就不再詳聊了,下面就講講如何將阿裡給出的demo運行起來。
支付寶在調用時,會首先看本地是不是存在支付寶客戶端,如果有的話,就直接調用客戶端進行支付,如果沒有,則調用jar包中的H5頁面支付。
所以在測試時,需要有測試兩種情境:有支付寶客戶端和沒有支付寶客戶端的情況。
在demo中大家可以看到,有客戶端的demo也有服務端的demo,大家可能覺得需要服務端寫好之後,客戶端才能集成,其實並不是。整個流程是這樣的:
1,APP客戶端通過SDK發送支付請求 (客戶端處理)
2,SDK支付成功並同步返回支付結果(客戶端處理)
3,支付寶服務器向我們的服務器發送支付結果字符串(服務端處理)
客戶端:從上面的流程可以看出,服務端只是用來接出異步返回的支付結果的。而支付與同步結果返回都是在客戶端可以直接看得到的。所以在集成支付寶支付接口時,主要功能是在客戶端,即便服務端沒有做集成,也是可能付款成功的。
服務端:服務端只需要添加一個功能:接口支付結果返回
下面幾張圖顯示了整個demo的運行過程,由於沒辦法在真機上錄制gif,所以只能用圖片來代替了。
初始化界面:
點擊支付後,跳出確認付款界面:
點擊確認付款後,跳出輸入密碼界面:
最後是支付成功界面:
在看DEMO的代碼之前,我們需要先配置幾個變量:
這部分會對代碼中用到的幾個變量的找到方法或生成方法進行講述,部分資料引自支付寶開放平台。
合作者身份ID(PID)是商戶與支付寶簽約後,商戶獲得的支付寶商戶唯一識別碼。當商戶把支付寶功能接入商戶網站時會用到PID,以便讓支付寶認證商戶。
查看PID步驟如下:
1、登錄支付寶官方網站b.alipay.com
2、點擊導航欄中“商家服務”
3、點擊“查詢PID、Key”
在https://openhome.alipay.com/platform/createApp.htm頁面,創建一個應用
完成之後:在我的應用中是可以看得到的:
然後轉到帳戶基本信息頁面:https://openhome.alipay.com/platform/keyManage.htm
在開放平台密鑰欄,可以找到APPID,APP SECRET,和支付寶密鑰
這三個數據,都是在應用創建後,支付寶為我們生成好的,無法更改!
(有關mac的生成方法,下面會再補充)
1、下載DEMO及SDK
到文檔中心,查看移動支付對應的文檔,文檔地址:http://doc.open.alipay.com/doc2/detail?treeId=59&articleId=103563&docType=1
然後,點擊(SDK&DEMO下載)下載代碼
2、得到原始私鑰
在代碼中的DEMO/openssl/bin目錄下,有openssl.exe文件
打開openssl.exe
輸入
genrsa-outrsa_private_key.pem1024
得到生成成功的結果,如下圖:
此時,我們可以在bin文件夾中看到一個文件名為rsa_private_key.pem的文件
用記事本方式打開它,可以看到-----BEGIN RSA PRIVATE KEY-----開頭,-----END RSA PRIVATE KEY-----結尾的沒有換行的字符串,這個就是原始的私鑰。
但這段原始私鑰代碼中是用不到的,我們需要將它轉化為PKCS8格式
3、轉換為PKCS8格式
在openssl.exe中輸入:並回車
pkcs8-topk8-informPEM-inrsa_private_key.pem-outformPEM-nocrypt
得到生成功的結果,這個結果就是PKCS8格式的私鑰,如下圖:
注意,私鑰是紅框包括的那部分,是不包含BEGIN PRIVATE KEY和END PRIVATE KEY這兩行的。
右鍵點擊openssl窗口上邊邊緣,選擇編輯→標記,選中要復制的文字(如上圖),
此時繼續右鍵點擊openssl窗口上邊邊緣,選擇編輯→復制,
把復制的內容粘土進一個新的記事本中,可隨便命名,只要知道這個是PKCS8格式的私鑰即可。
這裡來講一下mac端如何生成用戶私鑰的,由於mac系統是自帶openssl的,所以只需要打開終端,利用cd 命令切到任意一個想存放生成Key的文件夾下:
比如,切到下載目錄下
然後運行下面的命令來生成私鑰原始密鑰
opensslgenrsa-outrsa_private_key.pem1024
然後運行下面的命令來生成轉換的PCKS8格式的命令。
opensslpkcs8-topk8-informPEM-inrsa_private_key.pem-outformPEM-nocrypt
然後將生成的私鑰復制保存起來。
從上面的命令可以看出,與windows相比,mac上需要在前面添加openssl指定運行的是openssl命令。其它命令是完全一致的。
1、生成公鑰
同樣對於windows用戶而言,直接在openssl.exe中輸入下面的命令:
rsa-inrsa_private_key.pem-pubout-outrsa_public_key.pem
同樣,如果是Mac的同學,輸入的命令應該是如下:
opensslrsa-inrsa_private_key.pem-pubout-outrsa_public_key.pem
得到生成成功的結果,如下圖:
此時,我們可以在bin文件夾中看到一個文件名為rsa_public_key.pem的文件,用記事本方式打開它,可以看到-----BEGIN PUBLIC KEY-----開頭,
-----END PUBLIC KEY-----結尾的沒有換行的字符串,這個就是公鑰。
在生成網頁以後,復制----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----之間的部分,即那段純代碼,不要把----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----給復制進去了。中間的這部分就是公鑰。
2、網頁填充
然後到https://openhome.alipay.com/platform/keyManage.htm?keyType=partner(需要登錄)中,左側找到合作伙伴密鑰欄,再到右側的RSA加密中,將公鑰粘貼進去。由於,我們已經粘貼進去了,所以這裡顯示查看開發者公鑰,在沒填之前寫的是“添加開發者公鑰”
到這裡,所有的准備工作都已經結束了。下面就是配置DEMO的過程了
在剛才下載的sdk&demo的源碼中,打開DEMO/客戶端demo/支付寶Android 15.0.1/alipay_demo工程
路徑如下:
在PayDemoActivity中配置幾個變量:
//PID
publicstaticfinalStringPARTNER="";
在這裡填上我們上面找到的PID;
//商戶收款賬號
publicstaticfinalStringSELLER="[email protected]";
然後在SELLER上寫上我們支付寶的登錄帳戶,即那個你申請移動支付的支付寶賬號
//支付寶公鑰
publicstaticfinalStringRSA_PUBLIC="";
然後在RSA_PUBLIC這裡填上支付寶公鑰
//商戶私鑰,pkcs8格式
publicstaticfinalStringRSA_PRIVATE="";
最後是填上RSA_PRIVATE對應的商戶私鑰,注意是PKCS8格式的。
私鑰這部分,注意是----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----之間的部分,即那段純代碼,不要把----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----給復制進去了。中間的這部分就是公鑰。
現在運行demo就直接可以支付了。
本文中對應的DEMO在文章底部給出。
通過上面的配置,demo應該就直接可以運行了,但這裡所涉及的代碼,我們再仔細看看
主要的支付與結果返回就是pay()這個函數,這裡完成了支付所需要的所有功能。代碼如下:
publicvoidpay(Viewv){
…………
//訂單信息
StringorderInfo=getOrderInfo("測試的商品","該測試商品的詳細描述","0.01");
//對訂單做RSA簽名
Stringsign=sign(orderInfo);
try{
//僅需對sign做URL編碼
sign=URLEncoder.encode(sign,"UTF-8");
}catch(UnsupportedEncodingExceptione){
e.printStackTrace();
}
//完整的符合支付寶參數規范的訂單信息
finalStringpayInfo=orderInfo+"&sign=\""+sign+"\"&"
+getSignType();
RunnablepayRunnable=newRunnable(){
@Override
publicvoidrun(){
//構造PayTask對象
PayTaskalipay=newPayTask(PayDemoActivity.this);
//調用支付接口,獲取支付結果
Stringresult=alipay.pay(payInfo);
Messagemsg=newMessage();
msg.what=SDK_PAY_FLAG;
msg.obj=result;
mHandler.sendMessage(msg);
}
};
//必須異步調用
ThreadpayThread=newThread(payRunnable);
payThread.start();
}
這裡總是分了四步來完成支付與結果接收。
StringorderInfo=getOrderInfo("測試的商品","該測試商品的詳細描述","0.01");
主要是這句,即在getOrderInfo()函數中完成定單信息的構造:(這裡對getOrderInfo函數做的精減,更多字段及意義參考源碼)
有關paymethod的方法使用,參考:https://cshall.alipay.com/support/help_detail.htm?help_id=476935
各個字段的意義及取值參考:http://doc.open.alipay.com/doc2/detail?treeId=59&articleId=103663&docType=1
publicStringgetOrderInfo(Stringsubject,Stringbody,Stringprice){
//簽約合作者身份ID
StringorderInfo="partner="+"\""+PARTNER+"\"";
//簽約賣家支付寶賬號
orderInfo+="&seller_id="+"\""+SELLER+"\"";
//商戶網站唯一訂單號
orderInfo+="&out_trade_no="+"\""+getOutTradeNo()+"\"";
//商品名稱
orderInfo+="&subject="+"\""+subject+"\"";
//商品詳情
orderInfo+="&body="+"\""+body+"\"";
//商品金額
orderInfo+="&total_fee="+"\""+price+"\"";
//服務器異步通知頁面路徑
orderInfo+="¬ify_url="+"\""+"http://notify.msp.hk/notify.htm"
+"\"";
…………
returnorderInfo;
}
這裡就是通過我們的提供的商家ID,產品信息,價格等信息來構造定單及回調頁面,這裡需要非常注意的一個地方:
//服務器異步通知頁面路徑
orderInfo+="&noify_url="+"\""+"http://notify.msp.hk/notify.htm"
+"\"";
服務器異步通知頁面路徑,首先我們用支付寶支付之後,支付寶會返回給我們兩個通知,一個是同步的,就是我們點擊支付後支付寶直接反饋給我們客戶端的信息,我們可以直接拿到,根據反饋的結果可以初步判定該次交易是否成功,第二個就是服務器異步的通知,這個異步的通知是支付寶的服務器端發給我們服務器端的信息,我們在客戶端是直接獲取不了的,那支付寶的服務器怎麼知道我們服務器的路徑呢,那就是這參數的作用了,我們給支付寶服務器一個路徑,它就會在訂單狀態改變的時候給我們服務器端一個反饋,告訴服務器這次交易的狀態,如果服務器結果判定該次交易成功了,就必須返給支付寶服務器一個success,要不服務器會一直給我們異步通知,因為它不知道該次交易是否完成了(一般情況下25小時內8次通知,頻率一般是2m 10m 10m 1h 2h 6h 15h),我們一般會在收到異步通知時,對訂單的狀態進行更新。
其它的就不講了,通過看源碼都能看得懂,比如構造訂單號啥的。
為什麼要簽名呢?當然是防止傳輸出錯了,這可是跟錢相關的,如果orderInfo傳輸過程中出錯了,那怎麼樣來校驗它是不是出錯了呢,只有通過簽名算法來了。所以這裡就需要對訂單字符串做簽名。
具體簽名算法就不講了,直接應用到項目中就行,不需要理解,如果想看看怎麼實現的,裡面有對應的源碼,可以去研究一下。
//對訂單做RSA簽名
Stringsign=sign(orderInfo);
try{
//僅需對sign做URL編碼
sign=URLEncoder.encode(sign,"UTF-8");
}catch(UnsupportedEncodingExceptione){
e.printStackTrace();
}
在訂單字符串和簽名做完以後,就可以用他們來構造完整的請求字符串了:
//完整的符合支付寶參數規范的訂單信息
finalStringpayInfo=orderInfo+"&sign=\""+sign+"\"&"
+getSignType();
最後是發送請求,代碼如下:
RunnablepayRunnable=newRunnable(){
@Override
publicvoidrun(){
//構造PayTask對象
PayTaskalipay=newPayTask(PayDemoActivity.this);
//調用支付接口,獲取支付結果
Stringresult=alipay.pay(payInfo);
Messagemsg=newMessage();
msg.what=SDK_PAY_FLAG;
msg.obj=result;
mHandler.sendMessage(msg);
}
};
//必須異步調用
ThreadpayThread=newThread(payRunnable);
payThread.start();
最關鍵的部分在這裡:
PayTaskalipay=newPayTask(PayDemoActivity.this);
//調用支付接口,獲取支付結果
Stringresult=alipay.pay(payInfo);
Messagemsg=newMessage();
msg.what=SDK_PAY_FLAG;
msg.obj=result;
mHandler.sendMessage(msg);
在String result = alipay.pay(payInfo);中,就直接獲得了支付結果;
然後通過handler將結果發送出去。
3. 服務器集成
集成了客戶端,工作才剛剛完成一半,支付寶客戶端返回了支付結果後,應用服務器的數據庫中,訂單的狀態並沒有改變,如果依賴客戶端通知服務器改變訂單狀態,那是十分危險的,就好像剛剛說過的一樣,任何程序員經過一定的努力,都可能發送數據給你的應用服務器,改變你訂單的狀態,這樣,即使沒有支付,訂單的狀態也會變成已支付,所以,訂單狀態的改變,一定要依賴服務器的數據交互。
那麼當前端完成支付以後,數據如何交互呢?notify_url !
還記得前端構造的訂單數據中的notify_url麼,支付寶服務器就是利用這個配置,將服務器數據POST到這個地址上的,所以在進行服務器集成之前,你要保證你的服務器已經能夠接收到請求。
一份完整的代碼可以參考:https://github.com/hopeztm7500/AlipayServerDemo
1. 服務器同步的訂單數據
支付寶服務器發送給我們的數據,可以用如下的Bean來描述。
package com.wenxi.alipay.bean; import java.io.Serializable; import java.util.Date; import com.alibaba.fastjson.annotation.JSONField; public class AlipayNotification implements Serializable{ /** * */ private static final long serialVersionUID = -8638199167144867399L; private Integer alipayNoticeId; private String notifyId; private String notifyType; @JSONField(format="yyyy-MM-dd HH:mm:ss") private Date notifyTime; private String signType; private String sign; private String outTradeNo; private String subject; private String paymentType; private String tradeNo; private String tradeStatus; private String sellerId; private String sellerEmail; private String buyerId; private String buyerEmail; private Double totalFee; private Integer quantity; private Double price; private String body; @JSONField(format="yyyy-MM-dd HH:mm:ss") private Date gmtCreate; @JSONField(format="yyyy-MM-dd HH:mm:ss") private Date gmtPayment; private String isTotalFeeAdjust; private String userCoupon; private String discount; private String refundStatus; @JSONField(format="yyyy-MM-dd HH:mm:ss") private Date gmtRefund; private Boolean verifyResult; public String getNotifyId() { return notifyId; } public void setNotifyId(String notifyId) { this.notifyId = notifyId == null ? null : notifyId.trim(); } public String getNotifyType() { return notifyType; } public void setNotifyType(String notifyType) { this.notifyType = notifyType == null ? null : notifyType.trim(); } public Date getNotifyTime() { return notifyTime; } public void setNotifyTime(Date notifyTime) { this.notifyTime = notifyTime; } public String getSignType() { return signType; } public void setSignType(String signType) { this.signType = signType == null ? null : signType.trim(); } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign == null ? null : sign.trim(); } public String getOutTradeNo() { return outTradeNo; } public void setOutTradeNo(String outTradeNo) { this.outTradeNo = outTradeNo == null ? null : outTradeNo.trim(); } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject == null ? null : subject.trim(); } public String getPaymentType() { return paymentType; } public void setPaymentType(String paymentType) { this.paymentType = paymentType == null ? null : paymentType.trim(); } public String getTradeNo() { return tradeNo; } public void setTradeNo(String tradeNo) { this.tradeNo = tradeNo == null ? null : tradeNo.trim(); } public String getTradeStatus() { return tradeStatus; } public void setTradeStatus(String tradeStatus) { this.tradeStatus = tradeStatus == null ? null : tradeStatus.trim(); } public String getSellerId() { return sellerId; } public void setSellerId(String sellerId) { this.sellerId = sellerId == null ? null : sellerId.trim(); } public String getSellerEmail() { return sellerEmail; } public void setSellerEmail(String sellerEmail) { this.sellerEmail = sellerEmail == null ? null : sellerEmail.trim(); } public String getBuyerId() { return buyerId; } public void setBuyerId(String buyerId) { this.buyerId = buyerId == null ? null : buyerId.trim(); } public String getBuyerEmail() { return buyerEmail; } public void setBuyerEmail(String buyerEmail) { this.buyerEmail = buyerEmail == null ? null : buyerEmail.trim(); } public Double getTotalFee() { return totalFee; } public void setTotalFee(Double totalFee) { this.totalFee = totalFee; } public Integer getQuantity() { return quantity; } public void setQuantity(Integer quantity) { this.quantity = quantity; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } public String getBody() { return body; } public void setBody(String body) { this.body = body == null ? null : body.trim(); } public Date getGmtCreate() { return gmtCreate; } public void setGmtCreate(Date gmtCreate) { this.gmtCreate = gmtCreate; } public Date getGmtPayment() { return gmtPayment; } public void setGmtPayment(Date gmtPayment) { this.gmtPayment = gmtPayment; } public String getIsTotalFeeAdjust() { return isTotalFeeAdjust; } public void setIsTotalFeeAdjust(String isTotalFeeAdjust) { this.isTotalFeeAdjust = isTotalFeeAdjust == null ? null : isTotalFeeAdjust.trim(); } public String getUserCoupon() { return userCoupon; } public void setUserCoupon(String userCoupon) { this.userCoupon = userCoupon == null ? null : userCoupon.trim(); } public String getDiscount() { return discount; } public void setDiscount(String discount) { this.discount = discount == null ? null : discount.trim(); } public String getRefundStatus() { return refundStatus; } public void setRefundStatus(String refundStatus) { this.refundStatus = refundStatus == null ? null : refundStatus.trim(); } public Date getGmtRefund() { return gmtRefund; } public void setGmtRefund(Date gmtRefund) { this.gmtRefund = gmtRefund; } public Integer getAlipayNoticeId() { return alipayNoticeId; } public void setAlipayNoticeId(Integer alipayNoticeId) { this.alipayNoticeId = alipayNoticeId; } public Boolean getVerifyResult() { return verifyResult; } public void setVerifyResult(Boolean verifyResult) { this.verifyResult = verifyResult; } }
當然獲取這些數據需要一些額外的工作,首先它們存在Request對象中,其次它們是underscore case,所以Sample代碼中,先把這些數據用AsynAlipayNotifyController接收到,並且轉化成我們能夠處理的Bean格式。
其中,outTradeNo可能我們最關心的業務數據,是應用服務器中的訂單ID,狀態的改變幾乎全憑它。
2. 數據校驗
數據校驗的代碼,基本上就是支付寶Java服務器Demo中的代碼,需要配置的只有PID,代碼中已經提供了支付寶公鑰,這裡不需要修改。
數據校驗的核心代碼在這裡:
public static boolean verify(Mapparams) { // 判斷responsetTxt是否為true,isSign是否為true // responsetTxt的結果不是true,與服務器設置問題、合作身份者ID、notify_id一分鐘失效有關 // isSign不是true,與安全校驗碼、請求時的參數格式(如:帶自定義參數等)、編碼格式有關 String responseTxt = "false"; if (params.get("notify_id") != null) { String notify_id = params.get("notify_id"); responseTxt = verifyResponse(notify_id); } String sign = ""; if (params.get("sign") != null) { sign = params.get("sign"); } boolean isSign = getSignVeryfy(params, sign); if (isSign && responseTxt.equals("true")) { return true; } else { return false; } }
首先校驗通知ID,這個ID的是有有效期的,所以如果用測試數據POST到服務器,可能還要注意ID是不是過期,然後就是簽名校驗,簽名校驗使用了支付寶公鑰。然後根據校驗結果,就可以實現相應的業務邏輯了。
寫到這裡, 第三方支付-支付寶的應用集成,基本就理清了。不過,總結起來,技術都不是難點,如何讓用戶塞錢到你的支付寶,才是我們的最終目標~ 祝每個集成後的小伙伴,都有現金流進來!
實現刮刮卡我們可以Get到哪些技能?* 圓形圓角圖片的實現原理* 雙緩沖技術繪圖* Bitmap獲取像素值數據* 獲取繪制文本的長寬* 自定義View的掌握* 獲取屏幕密
Understanding AsyncTaskAsyncTask是Android 1.5 Cubake加入的用於實現異步操作的一個類,在此之前只能用Java SE庫中的T
本文開發一個基於Service的音樂播放器,音樂由後台運行的Service負責播放,當後台的播放狀態發生變化時,程序將會通過發送廣播通知前台Activity更新界面;當點
一、效果:我們看到很多軟件的通訊錄在右側都有一個字母索引功能,像微信,小米通訊錄,QQ,還有美團選擇地區等等。這裡我截了一張美團選擇城市的圖片來看看; 我們今天就來實現