日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

新冠疫情令我錯失了與朋友們見面、討論和玩紙牌游戲的機會。

 

Zoom 可以解決一些燃眉之急,但怎么玩紙牌游戲呢?怎么玩我們的Scopone呢?

 

于是,我決定開發(fā)一款可以與朋友們一起玩的 Scopone 游戲,同時在代碼中測試一些我著迷已久的架構(gòu)概念。

 

游戲的所有源代碼都可以找到在這個代碼庫里找到。

 

我想要哪些答案

 

自由部署服務(wù)器

 

一個支持多個玩家的交互式紙牌游戲是由客戶端和服務(wù)器端組成的。服務(wù)器部署在云端,但是在端的什么地方呢?是作為運行在專用服務(wù)器上的組件?還是作為 Kubernetes 托管集群中的 Docker 鏡像?或者是作為一個無服務(wù)器函數(shù)?

 

我不知道哪一個才是最好的選擇,但我關(guān)心的是游戲的核心邏輯的維護是否能夠獨立于部署模型。

 

獨立于 UI 框架或庫

 

“Angular 是最好的”。“不,React 更好也更快。”這樣的爭論無處不在。但這真的有關(guān)系嗎?難道我們不應(yīng)該將大部分前端邏輯作為純粹的 JAVAscript 或 Typescript 代碼,完全獨立于 UI 框架或庫嗎?我覺得是可以的,但還是想真正地去試一試。

 

自動測試多用戶交互場景的可能性

 

紙牌游戲與當(dāng)今其他交互式應(yīng)用程序一樣,都有多個用戶通過中央服務(wù)器進行實時交互。例如,當(dāng)玩家打出一張牌時,其他人都需要實時看到這張牌。一開始,我不清楚如何測試這類應(yīng)用程序。是否有可能使用簡單的 JavaScript 測試庫(如 Mocha)和標(biāo)準(zhǔn)測試實踐自動測試它?

 

Scopone 游戲可以回答我的問題

 

Scopone 游戲為我提供了一個很好的機會,讓我可以以一種具體的方式回答我自己提出的問題。所以,我決定嘗試實現(xiàn)它,看看我能從中學(xué)到什么。

 

整體思路

 

Scopone 游戲的規(guī)則

 

Scopone 是一種傳統(tǒng)的意大利紙牌游戲,4 名玩家分成 2 組,每組 2 人,有 40 張牌。

 

在游戲開始時,每個玩家都拿到 10 張牌,第一個玩家打出第一張牌,這張牌面朝上放在桌子上。然后第二個玩家出牌。如果這張牌的等級與桌上的牌相同,第二個玩家就從桌上“拿走”這張牌。如果桌上沒有牌,拿走牌的玩家就獲得“Scopa”的得分。然后第三個玩家出牌,并以此類推,直到所有牌都出完。

 

規(guī)則說完了,這里的關(guān)鍵點是,在玩家玩紙牌時,他們會改變游戲的狀態(tài),例如“哪些紙是正面朝上的”或“哪些玩家可以出下一張牌”。

 

應(yīng)用程序的結(jié)構(gòu)和技術(shù)棧

 

Scopone 游戲需要一個服務(wù)器實例和四個客戶端實例,四個玩家在他們的設(shè)備上啟動客戶端。

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

如果我們注意一下游戲中各種元素之間的互動,就可以知道:

 

  • 玩家執(zhí)行動作,例如玩家出牌;
  • 作為玩家執(zhí)行動作的結(jié)果,所有玩家都需要更新游戲的狀態(tài)。

 

這意味著客戶端和服務(wù)器需要一個雙向通信協(xié)議,因為客戶端必須向服務(wù)器發(fā)送命令,而服務(wù)器需要向客戶端推送更新后的狀態(tài)。WebSocket 是一種適合用在此處的協(xié)議,各種編程語言都支持它。

 

服務(wù)器端是用 Go 語言實現(xiàn)的,因為它對 WebSocket 有很好的支持,也支持不同的部署模型,換句話說,它可以部署成專用的服務(wù)器、Docker 鏡像或 Lambda。

 

客戶端是一個基于瀏覽器的應(yīng)用程序,以兩種不同的方式實現(xiàn):一種是 Angular,另一種是 React。這兩個版本都使用了 TypeScript 和 RxJs,以實現(xiàn)響應(yīng)式設(shè)計。

 

下圖是游戲的總體架構(gòu)。

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

命令和事件

 

簡而言之,這個游戲的過程是這樣的:

 

  • 客戶端通過消息向服務(wù)器發(fā)送命令;
  • 服務(wù)器更新游戲狀態(tài);
  • 服務(wù)器通過一條消息將游戲的最新狀態(tài)推送給客戶端;
  • 當(dāng)客戶端接收到來自服務(wù)器的消息時,將其視為觸發(fā)客戶端狀態(tài)更新的事件。

 

這個循環(huán)會一直重復(fù),直到游戲結(jié)束。

 

自由部署服務(wù)器端

 

服務(wù)器接收客戶端發(fā)送的命令消息,并根據(jù)這些命令更新游戲的狀態(tài),然后將更新后的狀態(tài)發(fā)送給客戶端。

 

客戶端通過 WebSocket 通道發(fā)送命令消息,它將被轉(zhuǎn)換成對服務(wù)器特定 API 的調(diào)用。

 

API 調(diào)用會生成響應(yīng),它將被轉(zhuǎn)換成一組消息,這些消息通過 WebSocket 通道發(fā)送給每個客戶端。

 

因此,在服務(wù)器端有兩個不同的層,它們有不同的職責(zé):游戲邏輯層和 WebSocket 機制層。

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

游戲邏輯層

 

這個層負(fù)責(zé)實現(xiàn)游戲邏輯,即根據(jù)接收到的命令更新游戲狀態(tài),并返回最新的狀態(tài),發(fā)送給每個客戶端。

 

因此,這個層可以使用內(nèi)部狀態(tài)和一組實現(xiàn)命令邏輯的 API 來實現(xiàn)。API 將向客戶端返回最新的狀態(tài)。

 

WebSocket 機制層

 

這個層負(fù)責(zé)將從 WebSocket 通道接收到的消息轉(zhuǎn)換為相應(yīng)的 API 調(diào)用。此外,它也需要將更新后的狀態(tài)(調(diào)用 API 生成的響應(yīng))轉(zhuǎn)換為推送給相應(yīng)的客戶端的消息。

 

層之間的依賴關(guān)系

 

基于前面的討論,游戲邏輯層獨立于 WebSocket,只是一組返回狀態(tài)的 API。

 

WebSocket 機制層實現(xiàn)了 WebSocket 特性,這一層將依賴所選擇的部署模型。

 

例如,如果我們決定將服務(wù)器端作為一個專用的服務(wù)器進行部署,那就需要選擇實現(xiàn)了 WebSocket 協(xié)議的包(在這里我們選擇了Gorilla),而如果我們決定作為 AWS Lambda 函數(shù)進行部署,那就需要依靠 WebSocket 協(xié)議的 Lambda 實現(xiàn)。

 

如果我們要保持游戲邏輯層與 WebSocket 機制層嚴(yán)格分離,就是在后者中導(dǎo)入前者(單向的),那么游戲邏輯層就不管擔(dān)心所選擇的具體部署模型是哪個。

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

基于這種策略,我們可以只開發(fā)單個版本的游戲邏輯,并自由地在各個地方部署服務(wù)器。

 

這有幾個好處。例如,在開發(fā)客戶端時,我們可以在本地運行 Gorilla WebSocket 實現(xiàn),這樣會非常方便,甚至可以在 VSCode 中啟用調(diào)試模式。這樣就可以在服務(wù)器代碼中設(shè)置斷點,通過客戶端發(fā)送的各種命令來調(diào)試游戲邏輯。

 

在將游戲部署到生產(chǎn)環(huán)境的服務(wù)器時(這樣就可以與我的朋友們實時游戲),可以直接將相同的游戲邏輯部署到云端,例如谷歌應(yīng)用程序引擎(GAE)。

 

此外,當(dāng)我發(fā)現(xiàn)不管我們有沒有在玩游戲,谷歌都會收取最低的費用(GAE 總是保持至少一個服務(wù)器打開),我可以在不改變游戲邏輯代碼的情況下將服務(wù)器遷移到 AWS Lambda 的“按需”收費模型。

 

獨立于 UI 框架或庫

 

現(xiàn)在的大問題是:選擇 Angular 還是 React?

 

我也問了自己另一個問題:是否有可能用 TypeScipt 開發(fā)大部分的客戶端邏輯,獨立于用來管理視圖的前端框架或庫?

 

結(jié)果證明,至少在這個案例中,它是可能的,只是有一些有趣的副作用。

 

應(yīng)用前端的設(shè)計:視圖層和服務(wù)層

 

應(yīng)用程序前端部分的設(shè)計有三個簡單的想法:

 

  • 客戶端分為兩層:
  • 視圖層是可組合的組件(Angular 和 React 都可以將 UI 作為組件的組合),可以實現(xiàn)純表示邏輯。
  • 服務(wù)層,用 TypeScript 實現(xiàn),不任何 Angular 或 React 的狀態(tài)管理,自己處理調(diào)用遠程服務(wù)器的命令和解釋來自服務(wù)器端的狀態(tài)變更響應(yīng)。
  • 服務(wù)層為視圖層提供了兩種類型的 API:
  • 公共方法——通過調(diào)用這些方法來調(diào)用遠程服務(wù)器上的命令,或者說是更改客戶端的狀態(tài)。
  • 公共事件流——實現(xiàn)為 RxJs Observable,可以被任何想要得到狀態(tài)變化通知的 UI 組件訂閱。
  • 視圖層只有兩個簡單的職責(zé):
  • 攔截 UI 事件并將其轉(zhuǎn)換為對服務(wù)層公共 API 方法的調(diào)用。
  • 訂閱公共 API Observable,并對接收到的通知做出相應(yīng)的表示更改。

 

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

一個視圖-服務(wù)-服務(wù)器交互示例

 

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

玩家可以通過點擊牌面打出一張牌

 

更具體一點,我們來看一下怎樣打出一張牌。

 

我們假設(shè) Player_X 將要打下一張牌。Player_X 點擊“紅桃 A”牌面,這個 UI 事件會觸發(fā)“Player_X 打出紅桃 A”這個動作。

 

以下是應(yīng)用程序?qū)?jīng)歷的步驟:

 

  • 視圖層攔截用戶生成的事件,并調(diào)用服務(wù)層的 playCard 方法,參數(shù)為“紅桃 A”。
  • 服務(wù)層向遠程服務(wù)器發(fā)送消息“Player_X 打出紅桃 A”。
  • 遠程服務(wù)器更新游戲的狀態(tài),并通知所有客戶端狀態(tài)發(fā)生了變化。例如,它告訴所有客戶端 Player_X 打了哪張牌以及誰是下一個可以出牌的玩家。
  • 每個客戶端的服務(wù)層都接收到由遠程服務(wù)器發(fā)送的狀態(tài)更新消息,并通過 Observable 流轉(zhuǎn)化為特定事件的通知。例如,Player_X 的客戶端服務(wù)層接收到的 isMyTurnToPlay$為 false,因為 Player_X 肯定不是下一個玩家。如果另一個玩家是 Player_Y, Player_Y 客戶端的務(wù)層接收到的 isMyTurnToPlay$將是 true。
  • 每個客戶端的視圖層都訂閱了由服務(wù)層發(fā)布的事件流,并對事件通知作出反應(yīng),按需更新 UI。例如,Player_Y(下一個玩家)的視圖層讓客戶端打出一張牌,而其他玩家的客戶端就不會有這個動作。

 

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

視圖層與服務(wù)層的交互

 

輕組件和重服務(wù)

 

基于這些規(guī)則,我們最終構(gòu)建了“輕組件”,它只管理 UI 關(guān)注點(表示和 UI 事件處理),而“重服務(wù)”則負(fù)責(zé)處理所有的邏輯。

 

最重要的是,“重服務(wù)”(包含大部分邏輯)完全獨立于所使用的 UI 框架或庫。它既不依賴 Angular 也不依賴 React。

 

有關(guān) UI 層的更多細節(jié)可以在本文的附錄部分找到。

 

這樣做的好處

 

這么做的好處是什么?

 

當(dāng)然不是不同的框架和庫之間的可移植性。一旦選擇了 Angular,就不太可能有人想要切換到 React,反之亦然,但還是有些優(yōu)勢的。

 

這種方法的一個優(yōu)點是,如果實現(xiàn)得徹底,它將標(biāo)準(zhǔn)化我們開發(fā)前端的方式,并更易于理解。歸根到底,這也只是通過定制的方式(服務(wù)層就是定制的)設(shè)計單向信息流。定制具有較低的抽象級別,也更簡單,但可能需要付出一些“重新發(fā)明輪子”的代價。

 

不過,最大的好處在于應(yīng)用程序具有更好和更容易的可測試性。

 

UI 測試是非常復(fù)雜的,無論你使用的是哪個框架或庫。

 

但如果我們將大部分代碼轉(zhuǎn)換為純 TypeScript 實現(xiàn),測試就會變得更容易。我們可以使用標(biāo)準(zhǔn)測試框架來測試應(yīng)用程序的核心邏輯(在這里我們使用了Mocha),我們還可以用一種相對簡單的方式來處理復(fù)雜的測試場景,我們將在下一節(jié)討論。

 

自動測試實時多用戶交互場景

 

Scopone 是一個四人游戲。

 

4 個客戶端必須通過 WebSocket 連接到一個中央服務(wù)器。一個客戶端執(zhí)行的操作,例如“打出一張牌”,會觸發(fā)所有客戶端的更新(也就是所謂的副作用)。

 

這是一種實時多用戶交互場景。這意味著如果我們想要測試整個應(yīng)用程序的行為,需要同時運行多個客戶端和一個服務(wù)器端。

 

我們該如何自動測試這些場景?我們可以用標(biāo)準(zhǔn)的 JavaScript 測試庫來測試它們嗎?我們可以在獨立的開發(fā)者工作站上測試它們嗎?這些是接下來要回答的問題。事實證明,所有這些事情都是可能的,至少在很大程度上是可能的。

 

實時多用戶交互場景測試的是什么

 

舉一個簡單的例子,假設(shè)我們想要測試游戲開始時所有玩家的紙牌分發(fā)是正確的。在新游戲開始后,所有客戶端都會從服務(wù)器收到 10 張牌(Scopone 游戲有 40 張牌,每個玩家可以拿到 10 張)。

 

如果我們想在一臺獨立的機器(比如,開發(fā)者的機器)上自動測試這種行為,就需要一個本地服務(wù)器。我們可以這樣做,因為服務(wù)器端可以作為一個本地的容器或 WebSocket 服務(wù)器運行。所以,我們假設(shè)有一個本地服務(wù)器運行在我們的機器上。

 

但是,為了運行測試,我們還需要找到一種方法來創(chuàng)建合適的上下文環(huán)境以及可以觸發(fā)我們想測試的副作用的動作(紙牌的分發(fā)就是一個玩家開始游戲的副作用)。換句話說,我們需要找到一種方法來模擬以下的情況:

 

  • 4 個玩家啟動應(yīng)用程序并加入同一個游戲(創(chuàng)建正確的上下文環(huán)境);
  • 一個玩家開始游戲(觸發(fā)我們想要測試的副作用)。

 

只有這樣我們才能檢查服務(wù)器是否將預(yù)期的牌發(fā)給所有玩家。

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

多用戶場景的一個測試用例

 

如何模擬多個客戶端

 

每個客戶端由一個視圖層和一個服務(wù)層組成。

 

服務(wù)層的 API(方法和 Observable 流)是在一個類中定義的(ScoponeServerService 類)。

 

每個客戶端創(chuàng)建這個類的一個實例,并連接到服務(wù)器。視圖層與它的服務(wù)類實例進行交互。

 

如果我們想要模擬 4 個客戶端,就創(chuàng)建 4 個不同的實例,并將它們?nèi)窟B接到我們的本地服務(wù)器。

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

創(chuàng)建 4 個服務(wù)類實例,代表 4 個不同的客戶端

 

如何為測試創(chuàng)建上下文

 

現(xiàn)在,我們有了 4 個已經(jīng)連接到服務(wù)器的客戶端,我們需要為測試構(gòu)建正確的上下文。我們需要 4 個玩家,并等待他們加入游戲。

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

為測試創(chuàng)建上下文

 

最后,如何執(zhí)行測試

 

在創(chuàng)建了 4 個客戶端和正確的上下文之后,我們就可以運行測試了。我們可以讓一個玩家發(fā)送命令開始游戲,然后檢查每個玩家是否收到了預(yù)期的紙牌數(shù)量。

架構(gòu)概念探索:以開發(fā)紙牌游戲為例

 

運行測試

 

合在一起

 

多用戶交互場景的測試如下:

 

  • 為每個用戶創(chuàng)建一個服務(wù)實例;
  • 按照正確的順序向服務(wù)發(fā)送命令,創(chuàng)建測試的上下文;
  • 發(fā)送觸發(fā)副作用的命令(就是被測試的命令);
  • 驗證每個服務(wù)的 Observable API 發(fā)出的通知,也就是命令的結(jié)果(副作用),是否包含了預(yù)期的數(shù)據(jù)。

 

這就是服務(wù)層 API 的 BDD

 

我們可以將這種方法視為針對服務(wù)層 API 的行為驅(qū)動開發(fā)(BDD)測試。

 

按照 BDD 的規(guī)范,測試行為是這樣的:

 

  • 假設(shè)初始情境:4 名玩家加入游戲;
  • 時間:玩家開始游戲;
  • 然后:我們希望每個玩家拿到 10 張牌。

 

測試函數(shù)是用一種 DSL 編寫的,它由一些特別的輔助函數(shù)組成,這些函數(shù)的組合創(chuàng)建了上下文(playersJoinTheGame 就是輔助函數(shù)的一個例子)。

 

它不是端到端測試,但可以非常強大

 

這不是一個完整的端到端測試。我們并沒有測試視圖層。

 

但它仍然可以是一個非常強大的工具,特別是如果我們堅持“輕組件和重服務(wù)”的規(guī)則。

 

如果視圖層由輕組件組成,并且大部分邏輯都集中在服務(wù)層,那么我們就能夠覆蓋應(yīng)用程序行為的核心,不管是客戶端的還是服務(wù)器端的,我們只需要進行相對簡單的設(shè)置,使用標(biāo)準(zhǔn)的工具(我們使用了 Mocha 測試庫,它絕對不是最新最閃亮的框架),并且是在開發(fā)人員的機器上進行。

 

這樣做的好處是,開發(fā)人員可以編寫出能夠快速執(zhí)行的測試套件,提高執(zhí)行測試的頻率。同時,這樣的測試套件實際上測試了從客戶端到服務(wù)器的整個應(yīng)用程序邏輯(即使是多用戶實時應(yīng)用程序),提供了很高的可信度。

 

結(jié)論

 

開發(fā)紙牌游戲是一種有趣的體驗。

 

除了在疫情期間為我?guī)硪恍啡ぶ猓€讓我有機會通過代碼來探索一些架構(gòu)概念。

 

我們經(jīng)常用架構(gòu)概念來表達我們的觀點。我發(fā)現(xiàn),將這些概念付諸實踐,即使是簡單的概念驗證,也會增加我們對它們的理解,讓我們更有信心在實際項目中使用它們。

 

附錄:視圖層機制

 

視圖層中的組件主要做了兩件事情:

 

  • 處理 UI 事件并將它們轉(zhuǎn)換為服務(wù)的命令。
  • 訂閱由服務(wù)公開的流,并通過更新 UI 來響應(yīng)事件。

 

為了更具體地說明最后一點的含義,我們可以舉一個例子:如何確定誰是下一個出牌的玩家。

 

正如我們所說的,這個游戲的一個規(guī)則是玩家可以一張接一張地出牌。例如,如果 Player_X 是第一個玩家,Player_Y 是第二個玩家,那么在 Player_X 出了一張牌之后,只有 Player_Y 才能出下一張牌,其他玩家都不能出牌。這個信息是服務(wù)器維護的狀態(tài)的一部分。

 

每次出了一張牌時,服務(wù)器就會向所有客戶端發(fā)送一條消息,指定下一個玩家是誰。

 

服務(wù)層通過一個叫作 enablePlay$的 Observable 流將消息轉(zhuǎn)換為通知。如果消息說玩家可以出下一張牌,服務(wù)層通過 enablePlay$通知的值為 true,否則就為 false。

 

讓玩家出牌的組件必須訂閱 enablePlay$流,并對通知的數(shù)據(jù)做出相應(yīng)的反應(yīng)。

 

在我們的 React 實現(xiàn)中,這是一個叫作 Hand 的功能組件。這個組件定義了一個狀態(tài)變量 enablePlay,它的值代表出牌的可能性。Hand 組件訂閱了 enablePlay$ Observable 流,每當(dāng)它收到 enablePlay$的通知時,就通過設(shè)置 enablePlay 的值來觸發(fā) UI 重繪。

 

下面是使用 React 的 Hand 組件實現(xiàn)這個特定功能的相關(guān)代碼。

export const Hand: FC = () => {
 const server = useContext(ServerContext);
  . . .
 const [handReactState, setHandReactState] = useState<HandReactState>({
   . . .
   enablePlay: false,
 });
  . . .
 useEffect(() => {
  . . .
  . . .
   const enablePlay$ = server.enablePlay$.pipe(
     tap((enablePlay) => {
       setHandReactState((prevState) => ({ ...prevState, enablePlay }));
     })
   );




   const subscription = merge(
       . . .
     handClosed$
   ).subscribe();




   return () => {
     console.log("Unsubscribe Hand subscription");
     subscription.unsubscribe();
   };
 }, [server]);
  . . .




 return (
   <>
       . . .
       <Cards
           . . .
         enabled={handReactState.enablePlay}
       ></Cards>  
       . . .
   </>
 );
};

復(fù)制代碼

 

Angular 版本的邏輯是一樣的,并且是在 HandComponent 中實現(xiàn)的。唯一的區(qū)別是對 enablePlay$ Observable 流的訂閱是直接在模板中通過 async 管道完成的。

 

作者簡介:

 

Enrico Piccinin 對代碼和 IT 組織中偶爾發(fā)生的離奇事情很感興趣。憑借在 IT 開發(fā)領(lǐng)域多年的經(jīng)驗,他希望了解將“新 IT”應(yīng)用在傳統(tǒng)組織中會發(fā)生什么。你可以在 enricopiccinin.com 或 LinkedIn 上找到 Enrico。

 

原文鏈接

 

https://www.infoq.com/articles/exploring-architecture-building-game/

了解更多軟件開發(fā)與相關(guān)領(lǐng)域知識,點擊訪問 InfoQ 官網(wǎng):https://www.infoq.cn/,獲取更多精彩內(nèi)容!

分享到:
標(biāo)簽:架構(gòu)
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運動步數(shù)有氧達人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定