任何一個大型的應用程序和服務,都必須會使用到高性能的數據存儲解決方案,用來準確(ACID,原子性Atomicity、一致性Consistency、隔離性Isolation、持久性Durability,可以拓展了解一下)、快速、可靠地存儲和檢索用戶的賬戶信息、商品以及商品交易信息、產品數據、資訊文章等等等等,而云開發(fā)就自帶高性能、高可用、高拓展性且安全的數據庫。
在操作數據庫時,我們要對數據庫database、集合collection、記錄doc以及字段field要有一定的了解,首先要記住這些對應的英文單詞,當你要操作某個記錄doc的字段內容時,就像投送快遞一樣,要先搞清楚它到底在哪個數據庫、在哪個集合、在哪個記錄里,一級一級的去找。操作數據庫通常都是對數據庫、集合、記錄、字段進行增、刪、改、查,當你清楚了這些,操作數據庫就不會迷糊了。
我們可以結合Excel以及MySQL(之前沒有接觸過MySQL也沒有關系,只看與Excel的對應就行)來理解云開發(fā)的數據庫。
云數據庫 | MySQL數據庫 | Excel文件 |
---|---|---|
數據庫database | 數據庫 database | 工作簿 |
集合 collection | 表 table | 工作表 |
字段field | 數據列column | 數據表的每一列 |
記錄 record/doc | 記錄row | 數據表除開第一行的每一行 |
我們現在來創(chuàng)建一個books的集合(相當于創(chuàng)建一張Excel表),用來存放圖書館里面書籍的信息,比如這樣一本書:
書名title | JavaScript權威指南(第6版) | |
---|---|---|
作者author | 弗蘭納根(David Flanagan) | |
標準書號isbn | 9787111376613 | |
出版信息publishInfo | 出版社press | 機械工業(yè)出版社 |
出版年份year | 2012 |
打開云開發(fā)控制臺的數據庫標簽,新建集合books,然后選擇該集合,給books里添加記錄(類似于填寫Excel含字段的第一行和其中一行關于書的信息記錄),依次添加字段:
在數據庫創(chuàng)建之后,我們需要在云開發(fā)控制臺-數據庫-集合的權限設置標簽對數據庫進行權限設置。數據庫的權限分為小程序端和服務端(云函數、云開發(fā)控制臺)。服務端擁有讀寫所有數據的讀寫權限,所以這里的權限設置只是在設置小程序端的用戶對數據庫的操作權限。權限控制分簡易權限控制和自定義權限(也就是安全規(guī)則),建議開發(fā)者用安全規(guī)則取代簡易的權限控制。
技術文檔:權限控制
要使用自定義權限(也就是安全規(guī)則)來全面取代簡易的權限控制,我們需要了解4個簡易的權限控制所表示的意思,以及安全規(guī)則應該如何一一取代它們,也就是我們在配置集合的權限時,不再選擇簡易的權限控制,而是統(tǒng)一選擇自定義權限,填寫與之對應的json規(guī)則即可。
安全規(guī)則可以讓更加靈活而又明確地自定義前端數據庫讀寫權限的能力,通過配置安全規(guī)則,開發(fā)者可以精細化的控制集合中所有記錄的讀read、寫write權限。其中write權限還可以細分為create新建、update更新、delete刪除等權限,還支持比較、邏輯運算符進行更加精細化的權限配置。
所有用戶可讀,僅創(chuàng)建者可讀寫:比如用戶發(fā)的帖子、評論、文章,這里的創(chuàng)建者是指小程序端的用戶,也就是存儲UGC(用戶產生內容)的集合要設置為這個權限;
{
"read": true,
"write": "doc._openid == auth.openid"
}
僅創(chuàng)建者可讀寫:比如私密相冊,用戶的個人信息、訂單,也就是只能用戶自己讀與寫,其他人不可讀寫的數據集合;
{
"read": "doc._openid == auth.openid",
"write": "doc._openid == auth.openid"
}
所有人可讀:比如資訊文章、商品信息、產品數據等你想讓所有人可以看到,但是不能修改的內容;
{
"read": true,
"write": false
}
所有用戶不可讀寫:如后臺用的不暴露的數據,只能你自己看到和修改的數據;
{
"read": false,
"write": false
}
小程序端 API 擁有嚴格的調用權限控制,比如在小程序端A用戶是不能修改B用戶的數據的,沒有這樣的權限,在小程序端只能修改非敏感且只是針對單個用戶的數據;對于有更高安全要求的數據,我們可以在云函數內通過服務端 API 來進行操作。
如果數據庫集合里的數據是通過導入的方式獲取的,這個集合的權限默認為“僅創(chuàng)建者可讀寫”,這個權限在服務端(云函數)可以調用,但是在小程序端可能會返回空數組哦,所以一定要記得根據情況修改權限。
小程序端與云函數的服務端無論是在權限方面、API的寫法上(有時看起來一樣,但是寫法不一樣),還是在異步處理上(比如服務端不再使用success、fail、complete回調,而是返回Promise對象),都存在非常多的差異,這一點要分清楚。
查詢集合collection里的記錄是云開發(fā)數據庫操作最重要的知識,在上一節(jié)我們已經將中國城市經濟數據china.csv的數據導入到了集合china之中,并已經設置好了集合的權限為“所有人可讀,僅創(chuàng)建者可讀寫”(或使用安全規(guī)則),接下來我們就以此為例并結合中國城市經濟線上excel版來講解數據庫的查詢。在中國城市經濟線上excel版以及云開發(fā)控制臺china集合里,我們可以看到中國332個城市的名稱city、省份province、市區(qū)面積city_area、建成區(qū)面積builtup_area、戶籍人口reg_pop、常住人口resident_pop、GDP的數據。
查詢中國GDP在3000億元以上的前10個城市,并要求不顯示_id字段,顯示城市名、所在省份以及GDP,并按照GDP大小降序排列。
使用開發(fā)者工具新建一個chinadata頁面,然后再在index.js的onLoad生命周期函數里輸入以下代碼。操作集合里的數據涉及的知識點非常繁雜,下面的案例相對比較完整,便于大家有一個整體性的理解:
const db = wx.cloud.database() //獲取數據庫的引用
const _ = db.command //獲取數據庫查詢及更新指令
db.collection("china") //獲取集合china的引用
.where({ //查詢的條件指令where
gdp: _.gt(3000) //查詢篩選條件,gt表示字段需大于指定值。
})
.field({ //顯示哪些字段
_id:false, //默認顯示_id,這個隱藏
city: true,
province: true,
gdp:true
})
.orderBy('gdp', 'desc') //排序方式,降序排列
.skip(0) //跳過多少個記錄(常用于分頁),0表示這里不跳過
.limit(10) //限制顯示多少條記錄,這里為10
.get() //獲取根據查詢條件篩選后的集合數據
.then(res => {
console.log(res.data)
})
.catch(err => {
console.error(err)
})
大家可以留意一下數據查詢的鏈式寫法, wx.cloud.database().collection('數據庫名').where().get().then().catch(),前半部分是數據查詢時對對象的引用和方法的調用;后半部分是Promise對象的方法,Promise對象是get的返回值。寫的時候為了讓結構更加清晰,我們做了換行處理,寫在同一行也是可以的。
在上面的案例中,就包含了構建查詢條件的五個方法: Collection.where()、 Collection.field()、 Collection.orderBy()、 Collection.skip()、 Collection.limit(),這五個方法是可以單獨拆開使用的,比如只使用where或只使用field、limit,也可以從這5個中抽幾個組合在一起使用,還可以一次查詢里寫多個相同的方法,比如orderBy、where可以寫多次相同的。
不過值得注意的是這5個方法順序不同查詢的結果有時也會有所不同(比如orderBy多次打亂順序的情況下),查詢性能也會有所不同。通常skip最好放在后面,不要讓skip略過大量數據。skip().limit()和limit().skip()效果是等價的。構建查詢條件的5個方法是基于集合引用Collection的,就拿where來說,不能寫成 wx.cloud.database().where(),也不能是 wx.cloud.database().collection("china").doc.where(),只能是 wx.cloud.database().collection("china").where(),也就是只能用于查詢集合collection里的記錄。
技術文檔:Collection.where
技術文檔:Collection.field
技術文檔:Collection.orderBy
技術文檔:Collection.skip
技術文檔:Collection.limit
傳入的對象的每個 <key, value> 構成一個篩選條件,有多個 <key, value> 則表示需同時滿足這些條件,是 與的關系,如果需要 或關系,可使用 command.or
指令用于查詢時,都會寫在where內,主要對字段的值進行比較和邏輯的篩選判斷。數據庫 API 提供了大于、小于等多種查詢指令,這些指令都暴露在 db.command 對象上。
指令Command可以分為查詢指令和更新指令,這兩者的用法有很大的區(qū)別,查詢指令用于db.collection的where條件篩選,而更新指令則是用于db.collection.doc的update請求的字段的更新里,這兩者的區(qū)別在后面我們也會反復提及。
下面我們把查詢指令的比較操作符和邏輯操作符整理成了一張表格,并附上相應的技術文檔,方便大家對它們有一個清晰而整體的認識。 查詢指令之比較
查詢指令之比較 | |||
---|---|---|---|
gt | 大于 | lt | 小于 |
eq | 等于 | neq | 不等于 |
lte | 小于或等于 | gte | 大于或等于 |
in | 在數組中 | nin | 不在數組中 |
查詢指令之邏輯 | |||
and | 條件與 | or | 條件或 |
not | 條件非 | nor | 都不 |
指令command是基于database數據庫引用的,我們以大于gt在小程序端(以大于3000為例)的完整寫法為例:
wx.cloud.database().command.gt(3000)
為了簡便,通常我們會把 wx.cloud.database()會賦值給一個變量,如 db, db.command又會賦值給 ,使用時最終被簡化為 .gt(3000)。通過一層一層的聲明變量并賦值,大大簡化了指令的寫法,大家可以在其他指令都沿用這種寫法。
相比于其他的比較指令等于eq和不等于neq操作符的用法非常豐富,它可以進行數值比較,我們查詢某個字段比如GDP等于某個數值如17502.8億的城市:
.where({
gdp: _.eq(17502.8),
})
它還可以進行字符串的匹配,比如我們查詢某個字段比如city完整匹配一個字符串如深圳:
.where({
city: _.eq("深圳"),
})
注意:在查詢時,gdp: _.eq(17502.8)的效果等同于gdp:17502.8,而city: _.eq(“深圳”)等同于city:”深圳”,雖然兩種方式查詢的結果都是一致的,但是它們的原理不同,前者用的是等于指令,后者用的是傳遞對象值。
eq還可以用于字段的值是數組以及對象的情況,在后面的章節(jié)我們會再來介紹。
查詢廣東省內、GDP在3000億以上且在1萬億以下的城市。在廣東省內也就是讓字段province的值等于”廣東”,而GDP的要求則是GDP這個字段同時滿足大于3000億且小于1萬億,這時就需要用到and(條件與,也就是且的意思):
.where({
province:_.eq("廣東"),
gdp:_.gt(3000).and(_.lt(10000))
})
上面的案例中where內的兩個條件, province:.eq("廣東")和 gdp:.gt(3000).and(_.lt(10000))帶有跨字段的條件與and(也就是且)的關系,那如何實現跨字段的條件或or呢?
查詢中國GDP在3000億元以上且常住人口在500萬以上或建城區(qū)面積在300平方公里以上的前20個大城市。這里常住人口和建成區(qū)面積只需要滿足其中一個條件即可,這就涉及到條件或or(注意下面代碼的格式寫法):
.where(
{
gdp: _.gt(3000),
resident_pop:_.gt(500),
},
_.or([{
builtup_area: _.gt(300)}
]),
)
注意上面三個條件, gdp: _.gt(3000)和 residentpop:.gt(500)是邏輯與,而與 builtuparea: .gt(300)}的關系是邏輯或。 _.or([{條件一 },{條件二 }])內是一個數組,條件一與條件二又構成邏輯與的關系。
正則表達式能夠靈活有效匹配字符串,可以用來檢查一個串里是否含有某種子串,比如“CloudBase技術訓練營”里是否含有”技術”這個詞。云數據庫正則查詢支持UTF-8的格式,可以進行中英文的模糊查詢。正則查詢也是寫在where字段的條件篩選里。
技術文檔:Database.RegExp
我們可以用正則查詢來查詢某個字段,比如city城市名稱內,包含某個字符串比如”州”的城市:
.where({
city: db.RegExp({
regexp: '州',
options: 'i',
})
})
注意這里的city是字段,db.RegExp()里的regexp是正則表達式,而options是flag,i是flag的值表示不區(qū)分字母的大小寫。當然我們也可以直接在where內用JavaScript的原生寫法或調用 RegExp對象的構造函數。比如上面的案例也可以寫成:
//JavaScript原生正則寫法
.where({
city:/州/i
})
//JavaScript調用RegExp對象的構造函數寫法
.where({
city: new db.RegExp({
regexp: "州",
options: 'i',
})
})
數據庫查詢的正則表達式也支持模板字符串,比如我們可以先聲明const cityname=”州”,然后用模板字符串包住cityname變量:
city: db.RegExp({
regexp:`${cityname}`,
options: 'i',
})
正則表達式的用法是非常繁雜的,關于正則表達式的知識可以去搜索了解更多細節(jié)。
技術文檔:正則表達式
值得注意的是,在數據庫查詢時應盡可能避免過度使用正則表達式來做復雜的匹配,尤其是用戶訪問觸發(fā)較多的場景,通常情況下數據查詢的響應時間(無論是小程序端還是云函數端)最好要低于500ms。
在前面我們已經介紹了集合數據請求的查詢方法get,除了get查詢外,請求的方法還有add新增,remove刪除、update改寫/更新、count統(tǒng)計以及watch監(jiān)聽,這些方法都是基于數據庫集合的引用Collection的,接下來我們再來介紹如何基于Collection新增記錄和統(tǒng)計記錄的數量。
基于數據庫集合的引用Collection所查詢到的記錄都是多條記錄,也就是說我們可以對N條記錄進行增、刪、改、查等操作,不過目前還不支持在小程序端進行多條記錄的update和remove,只能在云函數端進行這樣的操作。
統(tǒng)計記錄Collection.count
統(tǒng)計集合記錄數或統(tǒng)計查詢語句對應的結果記錄數。小程序端與云函數端的表現會有如下差異:小程序端:注意與集合權限設置有關,一個用戶僅能統(tǒng)計其有讀權限的記錄數云函數端:因屬于管理端,因此可以統(tǒng)計集合的所有記錄數。
技術文檔:Collection.count()
const db = wx.cloud.database()
const _ = db.command
db.collection("china")
.where({
gdp: _.gt(3000)
})
.count().then(res => {
console.log(res.total)
})
field、orderBy、skip、limit對count是無效的,只有where才會影響count的結果,count只會返回記錄數,不會返回查詢到的數據。
在前面我們將知乎日報的數據導入到了zhihu_daily的集合里,接下來,我們就來給zhihu_daily新增記錄。
技術文檔:Collection.add
使用開發(fā)者工具新建一個zhihudaily的頁面,然后在zhihudaily.wxml里輸入以下代碼,新建一個綁定了事件處理函數為addDaily的button按鈕:
<button bindtap="addDaily">新增日報數據</button>
然后再在zhihudaily.js里輸入以下代碼,在事件處理函數addDaily里調用Collection.add,往集合zhihu_daily里添加一條記錄,如果傳入的記錄對象沒有 _id 字段,則由后臺自動生成 _id;若指定了 _id,則不能與已有記錄沖突。
addDaily(){
db.collection('zhihu_daily').add({
data: {
_id:"daily9718005",
title: "元素,生生不息的宇宙諸子",
images: [
"https://pic4.zhimg.com/v2-3c5d866701650615f50ff4016b2f521b.jpg"
],
id: 9718005,
url: "https://daily.zhihu.com/story/9718005",
image: "https://pic2.zhimg.com/v2-c6a33965175cf81a1b6e2d0af633490d.jpg",
share_url: "http://daily.zhihu.com/story/9718005",
body:"<p><strong><strong>謹以此文,紀念元素周期表發(fā)布 150 周年。</strong></strong></p>\r\n<p>地球,世界,和生活在這里的蕓蕓眾生從何而來,這是每個人都曾有意無意思考過的問題。</p>\r\n<p>科幻小說家道格拉斯·亞當斯給了一個無厘頭的答案,42;宗教也給出了諸神創(chuàng)世的虛構場景;</p>\r\n<p>最為恢弘的畫面,則是由科學給出的,另一個意義上的<strong>生死輪回,一場屬于元素的生死輪回</strong>。</p>"
}
})
.then(res => {
console.log(res)
})
.catch(console.error)
}
點擊新增日報數據的button,會看到控制臺打印的res對象里包含新增記錄的_id為我們自己設置的daily9718005。打開云開發(fā)控制臺的數據庫標簽,打開集合zhihu_daily,翻到最后一頁,就能看到我們新增的記錄啦。
注意和導入的數據不同的是,在小程序端新增記錄,都會自動添加一個_openid的字段,它的值等于用戶 openid,_openid的值是不允許修改的。當我們把集合的權限改為僅創(chuàng)建者可讀寫,或所有人可讀,僅創(chuàng)建者可讀寫,在小程序端查詢或更新記錄時,會自動添加一個條件,
.where({
_openid:"當前用戶的openid"
})
所以這就是為什么盡管集合里面有數據,但是由于有了這個條件,只要記錄里沒有_openid或openid不匹配就查詢不到記錄。
集合請求方法注意事項
get、update、count、remove、add等都是請求,在小程序端可以有callback和promise兩種寫法,但是在云函數端只能用promise,不能用callback。為了方便,建議大家統(tǒng)一使用promise的寫法,也就是then、catch。
get、update、count、remove、add請求不能在一個數據庫引用里同時存在。比如不能又是get(),又是count()的,不能這么寫:
db.collection('china').where({
_openid: 'xxx',
}).get().count().add()
在云開發(fā)能力章節(jié)我們已經介紹過如何在云函數端調用數據庫,這里也是一樣。新建一個云函數chinadata,然后在 exports.main = async (event, context) => {}輸入以下代碼,注意是 const db = cloud.database(),wx. cloud.database(),云函數端的數據庫引用和小程序端有所不同:
const db = cloud.database()
const _ = db.command
return await db.collection("china")
.where({
gdp: _.gt(3000)
})
.field({
_id: false,
city: true,
province: true,
gdp: true
})
.orderBy('gdp', 'desc')
.skip(0)
.limit(10)
.get()
try/catch async錯誤處理
當 async 函數中只要一個 await 出現 reject 狀態(tài),則后面的 await 都不會被執(zhí)行。如果有多個 await 則可以將其都放在 try/catch 中。
然后右鍵chinadata云函數根目錄選擇在終端中打開,輸入npm install,之后上傳并部署所有文件。
?
在前面我們了解到,調用云函數可以使用本地調試、云端測試,我們還可以在小程序端調用云函數,將云函數的數據返回到小程序端來。使用開發(fā)者工具在chinadata.wxml里輸入以下代碼,也就是我們通用點擊按鈕觸發(fā)事件處理函數:
<button bindtap="callChinaData">調用chinadata云函數</button>
再在事件處理函數里調用云函數,在chinadata.js里輸入getChinaData事件處理函數來調用chinadata云函數:
getChinaData() {
wx.cloud.callFunction({
name: 'chinadata',
success: res => {
console.log("云函數返回的數據",res.result.data)
},
fail: err => {
console.error('云函數調用失敗:', err)
}
})
},
在模擬器里點擊調用chinadata云函數的button按鈕,就能在控制臺里看到云函數返回的查詢到的結果,大家可以通過setData的方式將查詢的結果渲染到小程序頁面,這里就不介紹啦。
基于數據庫集合的引用Collection,我們可以先匹配 where 語句查詢到相關條件的多條記錄,再來調用Collection.remove()來進行刪除。五個查詢方法,skip和limit不支持,field、orderBy沒有意義,只有where條件可以用來篩選記錄。數據一旦刪除就不能再找回了。
技術文檔:Collection.remove()
我們可以把之前建好的chinadata云函數 exports.main = async (event, context) => {}里的代碼修改為如下,即刪除省份province為廣東的所有數據:
return await db.collection('china')
.where({
province:"廣東"
})
.remove()
在模擬器里點擊調用chinadata云函數的button按鈕,就能在控制臺里看到云函數返回的對象,其中包含stats: {removed: 22},即刪除了22條數據。
更新多條記錄Collection.update
我們可以把之前建好的chinadata云函數 exports.main = async (event, context) => {}里的代碼修改為如下,也就是先查詢省份province為湖北的記錄,給這個記錄更新一個字段英文省份名pro-en:
return await db.collection('china')
.where({
province:"湖北"
})
.update({
data: {
"pro-en": "Hubei"
},
})
這里要注意的是,pro-en這個字段之前是沒有的,通過Collection.update不只是起到更新的作用,還可以批量新增字段并賦值,也就是update時記錄里有相同字段就更新,沒有就新增; "pro-en": "Hubei",直接使用pro-en會報錯,用雙引號效果等價。
如果你想給導入的數據添加_openid字段,只用云函數是沒法實現的,因為云函數沒有用戶的登錄態(tài)。我們需要先在小程序端調用云函數比如login返回openid,再將openid的值再傳給chinadata云函數,才能給記錄添加openid。
在前面我們已經了解了基于集合引用Collection構建查詢條件的5個方法,以及一些請求方法,接下來我們來講一下基于集合記錄引用Document的四個請求方法:獲取單個記錄數據Document.get()、刪除單個記錄Document.remove()、更新單個記錄Document.update()、替換更新單個記錄Document.set()。和基于Collection不一樣的是,前者的增刪改查是可以批量多條的,而基于Document則是操作單條記錄。
查詢集合collection里的記錄常用于獲取文章、資訊、商品、產品等等的列表;而查詢單個記錄doc的字段值則常用于這些列表里的詳情內容。如果你在開發(fā)中需要增刪改查某個記錄的字段值,為了方便讓程序可以根據_id找到對應的記錄,建議在創(chuàng)建記錄的時候_id用程序有規(guī)則的生成。
集合里的每條記錄都有一個 _id 字段用以唯一標志一條記錄,_id 的數據格式可以是number數字,也可以是string字符串。這個_id是可以自定義的,當導入記錄或寫入記錄沒有自定義時系統(tǒng)會自動生成一個非常長的字符串。查詢記錄doc的字段field值就是基于_id的。
技術文檔:獲取單個記錄數據Document.get()
比如我們查詢其中知乎日報的一篇文章(也就是其中一條記錄)的數據,使用開發(fā)者工具zhihudaily頁面的zhihudaily.js的onLoad生命周期函數里輸入以下代碼(db不要重復聲明):
db.collection('zhihu_daily').doc("daily9718006")
.get()
.then(res => {
console.log('單個記錄的值',res.data)
})
.catch(err => {
console.error(err)
})
},
如果集合的數據是導入的,那_id是自動生成的,自動生成的_id是字符串string,所以doc內使用了單引號(雙引號也是可以的哦),如果你自定義的_id是number類型,比如自定義的_id為20191125,查詢時為doc(20191125)即可,這只是基礎知識啦。
技術文檔:刪除單個記錄Document.remove()
removeDaily(){
db.collection('zhihu_daily').doc("daily9718006")
.remove()
.then(console.log)
.catch(console.error)
}
技術文檔:更新單個記錄Document.update()
updateDaily(){
db.collection('zhihu_daily').doc("daily9718006")
.update({
data:{
title: "【知乎日報】元素,生生不息的宇宙諸子",
}
})
},
技術文檔:替換更新單個記錄Document.set()
setDaily(){
db.collection('zhihu_daily').doc("daily9718006")
.set({
data: {
"title": "為什么狗會如此親近人類?",
"images": [
"https://pic4.zhimg.com/v2-4cab2fbf4fe9d487910a6f2c54ab3ed3.jpg"
],
"id": 9717547,
"url": "https://daily.zhihu.com/story/9717547",
"image": "https://pic4.zhimg.com/v2-60f220ee6c5bf035d0eaf2dd4736342b.jpg",
"share_url": "http://daily.zhihu.com/story/9717547",
"body": "<p>讓狗從兇猛的野獸變成忠實的愛寵,涉及了宏觀與微觀上的兩層故事:我們如何在宏觀上馴養(yǎng)了它們,以及這些馴養(yǎng)在生理層面究竟意味著什么。</p>\r\n<p><img class=\"content-image\" src=\"http://pic1.zhimg.com/70/v2-4147c4b02bf97e95d8a9f00727d4c184_b.jpg\" alt=\"\"></p>\r\n<p>狗是灰狼(Canis lupus)被人類馴養(yǎng)后形成的亞種,至少可以追溯到 1 萬多年以前,是人類成功馴化的第一種動物。在這漫長的歲月里,人類的定向選擇強烈改變了這個馴化亞種的基因頻率,使它呈現出極高的多樣性,尤其體現在生理形態(tài)上。</p>"
}
})
}
更多建議: