全棧開發入門實戰:後臺管理系統

本文首發於 GitChat 平臺,免費 Chat,連接:全棧開發入門實戰:後臺管理系統javascript


感謝你打開了這篇 Chat,在閱讀以前,須要讓你瞭解一些事情。css

第一,本 Chat 雖然免費,不表明沒有價值,我會將我的全棧開發的經歷敘述給你,但願對你有一些幫助;
第二,文中所使用的技術棧並不是最新,也並不是最優。後臺管理系統更可能是 2B 端的產品,一般是業務優先。本 Chat 的目的是爲了讓你可以快速上手全棧開發。
第三,本 Chat 雖然名爲全棧開發,可是不會帶你作完一個完整的後臺管理系統項目。一是因爲篇幅有限,二是因爲時間關係,我的精力也有限。html

正文

本 Chat 的內容,正如 Chat 簡介中所描述,將分爲如下 5 大塊:前端

  1. 開發準備
  2. 前臺樣式
  3. 數據庫鏈接
  4. 先後臺交互
  5. 線上部署

你可能會發現,好像不知道要作什麼,沒錯,後臺管理系統通常都是企業內部定製開發,一般是對業務的數據管理,具體作什麼功能由業務決定,但多數功能都是圍繞着表格或者表單。java

上面列舉的僅僅是全棧開發的大體流程。首先,要作一些準備工做,例如:開發環境、編輯器環境以及依賴包配置等等工做;其次,咱們要選定一個後臺模版樣式,快速套用,實現業務功能。(固然,你要本身開發也行,但不建議這麼作,除非爲了學習);而後,根據業務作數據庫的設計,編寫後臺數據處理邏輯,以及先後臺數據交互等功能;最後,測試並部署上線。node

這裏的示例,將實現一個學生數據的在線管理需求,其實就是一個在線表格,包括添加,刪除功能,系統層面包括登陸退出功能。麻雀雖小,五臟俱全,總體架子搭好了,再在上面添加功能就簡單多了。好了,如今就開始全棧之旅吧。mysql

開發準備

啓動項目

首先要作的是,開發環境的安裝,這裏就很少說了,關於 Node 環境的安裝,默認你已經搞定了。nginx

既然採用 Express 做爲 Web 框架,Express 也是要安裝的,有了 Node 環境,安裝 Express 就簡單多了。咱們直接上手 Express 的腳手架,全棧開發關鍵要速度。git

npm install express-generator -g

必定記得是全局安裝。安裝完成以後,輸入 express -h 能夠查看幫助。這裏選用 ejs 模版引擎,爲何?由於我順手而已,這個不重要。找個合適的目錄,運行下面命令:es6

express -e node-web-fullstack-demo

生成項目目錄以後,首先要安裝依賴,以下命令:

cd example-node-web-fullstack
npm install

等待安裝完成,咱們就能夠啓動項目了,使用命令 npm start ,去瀏覽器中,打開網址:http://localhost:3000,看到寫着 Express 的首頁,表明你的項目啓動成功了。

編輯器環境配置

一個好的編碼環境,可讓你項目開發效率加倍。

首先介紹一個編輯器配置 EditorConfig,這是一個編輯器的小工具。它有什麼做用呢?簡而言之,就是讓你能夠在不一樣的編輯器上,得到相同的編碼風格,例如:空格縮進仍是 Tab 縮進?縮進幾個空格?

你可能以爲詫異,這個不是在編輯器上設置就能夠了嗎?沒錯,假設你從始至終都是在同一個電腦同一個編輯器上編碼,那麼能夠忽略它。若是存在多電腦配合,亦或是多個編輯器配合,那麼它就是神器。它幾乎支持全部的主流編輯器,不用單獨去編輯器中設置,配置文件就在項目中,用那個編輯器打開項目,都能得到一致的編碼風格。

使用方法很簡單,在根目錄中新建 .editorconfig 文件便可。

# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true

# Matches multiple files with brace expansion notation
# Set default charset
[*.*]
charset = utf-8

# 4 space indentation
[*.js]
indent_style = space
indent_size = 4

# Indentation override for all JS under lib directory
[views/**.ejs]
indent_style = space
indent_size = 2

上面這個示例,設置了 js, ejs 的縮進,並經過 insert_final_newline 設置了文檔末尾默認插入一個空行。還有一些有意思配置,建議你查看一下官方文檔,就明白了,很是好用的一個小插件。

代碼檢查工具

使用 js 編碼,建議最好使用一款代碼檢查的工具,否則寫到後面就會很尷尬,每每一個小的語法錯誤,會讓你抓狂很久。代碼檢查工具推薦 ESLint

使用方法也很簡單,首先安裝要它,npm install eslint --save-dev,代碼檢查工具一般只須要安裝在開發依賴裏便可。緊接着你應該設置一個配置文件:./node_modules/.bin/eslint --init,而後,你就能夠在項目根目錄下運行 ESLint:./node_modules/.bin/eslint yourfile.js 用來校驗代碼了。若是你使用的是 IDE 編碼,通常都會有 ESLint 的插件來校驗代碼,例如在 VS Code 中安裝 eslint 插件,就能夠實時校驗正在編輯的文件了。

配置文件裏的設置參數就多了,建議自行查看官方文檔。以下示例:

{
    "rules": {
        "semi": ["error", "always"],
        "no-undef": "off",
        "no-unused-vars": "off",
        "no-console": "off"
    },
    "env": {
        "node": true,
        "es6": true
    },
    "extends": "eslint:recommended",
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parserOptions": {
        "ecmaVersion": 2018,
        "sourceType": "module"
    }
}

有時候你可能會以爲很煩,默認推薦的 eslint 校驗規則管得太多了,上述中我就手動關掉了「不容許使用 console」「不容許存在未定義變量」等配置。有些人可能乾脆關掉了校驗,可是,我仍是強烈建議啓用,畢竟規範的編碼效率會更高。

前臺樣式

前面作了那麼多工做,但彷佛一點效果也看不到,彆着急,這一部分的內容,就能讓你直接看到效果。

前端的工做實際上是很是費時的,它包括頁面設計及佈局等等,每每一個頁面光調樣式就要花費很長的時間。假如你是想快速搭建一個後臺管理系統,不建議本身去寫前端頁面代碼。頗有可能,頁面樣式尚未出來,就已經放棄了。除非你是單純想要學習。

建議的作法是,快速套用一個前端模版庫,將大致的頁面結構搭建出來,而後在上面添加業務功能。 這裏採用 AdminLTE 這套模版庫,大概效果圖以下:

它是基於 bootstrap3 的後臺模版,幾乎再也不須要作 CSS 的開發,一會兒就解決了前端界面設計的大問題。下面來看看如何套用它。

下載安裝

推薦採用 bower 包管理器來安裝,安裝命令:bower install adminlte 等待下載就能夠了。下載完成後,你會發現bower_components/ 目錄裏多了好多目錄,這些都是它的依賴包。

接下來,咱們還要將整個項目下載下來,獲得它的示例代碼,打開其中的 index.html 文件,分析一下它的頁面結構,好便於後面的拆解。以下圖:

拆解頁面結構

這是後臺首頁的一個基本結構,分爲:main-headermain-sidebarcontent-wrappermain-footercontrol-sidebar ,因而,咱們按照這幾大模塊,拆分頁面,分別保存爲 layout 模版。在項目 views 下新建兩個目錄:backend 以及 frontend ,分別對應後臺頁面以及前臺頁面。

backend 裏再新建 layout 目錄以及 index.ejs 文件,layout 中用來保存通用的頁面代碼,index.ejs 則表明後臺首頁。拆解完的 index.ejs 代碼以下:

<% include ./layout/head.ejs %>
<!-- custom css files -->

<% include ./layout/header.ejs %>
<% include ./layout/main-sidebar.ejs %>

<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
  ... 頁面代碼 ...
</div>

<% include ./layout/control-sidebar.ejs %>
<% include ./layout/footer.ejs %>

<!-- custom javascript files -->

<% include ./layout/foot.ejs %>

相信你一眼就能看出上面這段代碼的意思,將一個頁面拆分後,再重組,這就是最終的首頁。其餘的全部頁面均可以經過這樣的方式,進行重組,咱們要寫的代碼,僅僅只是修改 <div class="content-wrapper"></div> 裏的頁面便可。

配置靜態資源目錄

如今的頁面,咱們還不能訪問,由於頁面中連接的 CSS 以及 JS 文件路徑都不對,模版裏引用的都是相對路徑,咱們須要將它改成本項目裏的絕對路徑。打開 layout/head.ejs 文件,部分代碼以下:

<!-- Bootstrap 3.3.7 -->
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css">
<!-- Ionicons -->
<link rel="stylesheet" href="bower_components/Ionicons/css/ionicons.min.css">
<!-- Theme style -->
<link rel="stylesheet" href="dist/css/AdminLTE.min.css">

套用模塊的好處就是,這些連接庫的地址基本上沒太大的變化,咱們只須要修改一下根目錄就能夠了。修改以前,咱們須要先配置一下項目的靜態資源目錄,打開項目根目錄下的 app.js 文件,加上一行代碼以下:

...
app.use(express.static(path.join(__dirname, 'public')));
// 新添靜態資源目錄 bower_componennts
app.use(express.static(path.join(__dirname, 'bower_components')));
...

新加的這句代碼意思是,將項目中 bower_components/ 目錄設置爲靜態資源訪問的根目錄,那麼以上的那些靜態資源,咱們就知道怎麼引入了,修改以下:

<!-- Bootstrap 3.3.7 -->
<link rel="stylesheet" href="/bootstrap/dist/css/bootstrap.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="/font-awesome/css/font-awesome.min.css">
<!-- Ionicons -->
<link rel="stylesheet" href="/Ionicons/css/ionicons.min.css">
<!-- Theme style -->
<link rel="stylesheet" href="/admin-lte/dist/css/AdminLTE.min.css">

一樣的,JS 文件的引用路徑也要進行修改,打開 footer.ejs 文件,以 bower_components/ 爲根目錄,修改對應的 JS 文件路徑便可。好了,該頁面全部引用的靜態資源路徑,都已經修改正確了,接下來就是編寫頁面路由了。

PS. 頁面中的 JS 庫並不是都與本身的項目相匹配,後續要根據實際狀況,進行增減。

編寫頁面路由

頁面準備好了,咱們須要去編寫訪問路由,在這點上,Node 就與 PHP 和 Java 有所區別了,Node Web 開發不須要再搭建一個 Web 服務器,PHP 須要 Apache ,Java 須要 Tomcat,而 Node 把它交給你,讓你去自定義。咱們打開項目 routes/index.js 路由文件,編寫代碼以下:

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

// My backend index page
router.get('/backend', function(req, res, next){
  res.render('backend/index.ejs', {});
});

module.exports = router;

咱們加了一個路由 /backend ,渲染了上面準備好的 index.ejs 頁面,好了,咱們重啓一下項目,哦,對,若是已經安裝了 supervisor ,不須要手動重啓了,直接訪問 http://localhost:3000/backend

細心的你可能發現,這個頁面有些元素顯示不正常,這是由於有些靜態資源,咱們沒有安裝而致使的加載失敗,這個無需擔憂,由於實際頁面還須要不少的時間去打磨,這個要根據實際業務來決定頁面的內容,這裏就不去展開了。

接下來的事情,就是改寫頁面代碼了,總體的樣式結構都有了,剩下就是對頁面的刪刪減減了。

數據庫鏈接

根據上面的內容,相信你可以添加完頁面路由。當所有頁面改寫完畢以後,咱們就至關於獲得了,一個靜態的後臺管理系統。要達到的效果就是:點擊左側相應的導航,能夠實現頁面跳轉,可是沒有實際的數據,頁面中的數據都是寫「死的」,由於咱們尚未鏈接數據庫,接下來的工做就涉及到數據庫的鏈接了。

設計數據表

無論你選擇哪個數據庫,首先都要作設計數據表,對於數據表的設計以及業務埋點等內容,均可以看成一本書來說,這裏不會有那麼詳細的內容,來教你如何設計數據表,以及業務功能埋點。但有個建議須要提一下:不要着急去學習 SQL 語言,你可能會說,不學習 SQL 怎麼設計數據表呢?怎麼操做數據庫呢?

這裏須要強調的是,不是不須要學,而是開始的時候,不用着急去學。全棧開發重要的是快,否則你全棧幹啥呢?在實際的場景中,業務纔是最重要的。

首先打開你的 Excel(或在線表格應用) 把表的設計作出來再說。例以下圖是我當時一個項目的數據表設計:

最重要的是要去作這個事情,而不是去學數據庫,別擔憂作得不夠好,一次一次的實戰會讓你愈來愈熟練。作完這個工做,全棧開發基本算是完成了 40%,在這個過程當中,必定要深刻去分析業務流程,把該用到的不應用到的都要考慮進去,前期能夠不作開發,可是必要的字段必定要預留。

編寫數據庫模塊

一般狀況下,最好找一個趁手的數據庫 GUI 工具,把數據庫以及相關的表建立出來。這一步作完以後就是編寫鏈接數據數據的代碼了,建議在項目的根目錄中,新建名爲 db 的目錄,用來存放全部關於數據庫操做的代碼。

這裏咱們選用 MySQL 數據庫,Node 鏈接 MySQL 還須要安裝一個數據庫模塊。在項目目錄,執行命令 npm install --save mysql 便可,安裝完成以後,就能夠經過下面的示例,嘗試鏈接數據庫了。

var mysql = require('mysql');
var connection = mysql.createConnection({
  host: 'localhost',
  user: 'me',
  password : 'secret',
  database : 'my_db'
});
connection.connect();
connection.query('SELECT 1 + 1 AS solution', function(err, rows, fields) {
  if (err) throw err;
  console.log('The solution is: ', rows[0].solution);
});
connection.end();

這是官方提供的一個簡單例子,從上面的例子能夠得出:使用createConnection(option)方法建立一個鏈接對象,而後鏈接對象的connect()方法建立鏈接,最後使用query()方法執行SQL語句,返回結果做爲回調函數的參數rows返回,rows爲數組類型。

一般狀況下,咱們會使用鏈接池進行鏈接,鏈接池具體實現原理就很少展開了,能夠查看一下官方文檔。下面直接給出使用示例。

// connect.js 使用getConnection方法
var mysql = require('mysql');
var config = require('./config.json');
// 將配置寫入配置文件中
var pool = mysql.createPool(config.mysql);
exports.querySQL = function(sql, callback){
    pool.getConnection(function(err,conn){
        conn.query(sql,function(err,rows,fields){
            callback(err,rows,fields); 
            conn.release();   // 不要忘了釋放
        });        
    });
}

使用的時候,直接使用querySQL方法便可,以下:

// db.js 查詢用戶信息
var connect = require('./connect.js');
exports.getUser = function(username,callback){
    var sql = 'select * from user where username = "' + username + '"';
    connect.querySQL(sql,function(err,rows,fields){
        callback(err,rows,fields);        
    });
};

上面示例中,直接將 sql 語句寫進代碼中了,經過執行 sql 語句來得到數據庫數據。

這種方式優勢是比較直觀,可是缺點就太多了。不安全拼接字符串容易錯可擴展性差等等。在實際項目中,咱們每每都會選用一款 ORM 框架來鏈接數據庫。

使用 ORM 框架

關於 Node 使用 ORM 框架的介紹,我以前單獨寫了一篇 Chat,一樣是免費的,這裏我就再也不贅述了。

請點擊連接查看:如何使用 Sequelize 框架快速進行 Node Web 開發

先後臺交互

一般狀況下,先後臺交互的開發會涉及到多個部門多個崗位協做完成,除了先後臺,近幾年也分離出中臺的概念,專門提供接口服務。

而對於全棧開發來講,這個環節顯然要簡單得多了,雖然工做流程能夠省,可是,必要的前中後臺的框架仍是要有的,分離開的好處是儘可能下降功能耦合性,便於後期維護。

前臺頁面獲取動態數據一般有的兩種方式:

  • 一是,被動獲取,頁面被渲染,後臺傳參給前臺;
  • 二是,主動獲取,前臺主動請求,異步獲取數據。

後臺傳參方式

關於頁面展現,前面已經講過了,根據模塊改寫頁面,而後編寫路由,就能夠進行訪問了。但問題是,目前頁面上的一些內容都是靜態的,例如:站點的名稱,Logo以及標題等等。

一般狀況下,這些內容不該該是寫死在頁面中的,而應該是動態的,從後臺傳來的,甚至應該是寫入數據庫的,未來作個修改頁面,就能夠隨時修改網站標題以及 Logo 等信息。

這裏就涉及到後臺傳參的相關知識點了。Express 框架給咱們提供了三種傳參的方式:

(1)應用級別的傳參

使用 app.locals 定義參數。例如:在 app.js 文件中,routes(app); 語句以前加入 app.locals.hello = "Node",咱們在任何頁面中,使用 hello 這個參數都是沒問題的。如何使用?建議先了解下模版引擎的用法,這裏是 ejs。

(2)路由級別的傳參

使用 res.locals 定義參數。例以下面例子:

app.use(function (req, res, next) {
    res.locals.web = {
        title: 'STU INFO',
        name: '數據管理',
        desc: '學生數據管理',
        verifier: 0
    };
    res.locals.userInfo = {
        username: 'admin',
        title: '管理員'
    };
    res.locals.url = req.url.split('?')[0];
    next();
});

經過一箇中間件,將站點信息寫入 res.locals 對象中,因而咱們在對應的頁面中,就可使用 web 以及 userInfo 等對象參數了。例以下面登錄頁面部分代碼(注意:這裏是 ejs 模版引擎的寫法):

<div class="login-logo">
    <a href="/" title="<%= web.desc %>"><b><%= web.title %> </b><%= web.name %></a>
</div>

(3)頁面級別的傳參

使用 res.render() 方法定義參數,這應該是最經常使用的一種方式,例如頁面的 title 屬性,每一個頁面都是不同的,因此在渲染模版的時候,傳入 title 參數,是最合適的。例如登錄頁面的路由:

router.get('/login', function(req, res, next) {
  res.render('login', {
      pageInfo:{
          title: '登錄頁面',
      }
  });
});

示例中,定義了一個 pageInfo 的對象,將其看成參數傳輸到了前臺。

前臺異步獲取

前臺主動獲取數據的例子不少,一般都是經過 Ajax 異步的方式。典型的例子就是在線表格功能,由於後臺管理系統幾乎離不開在線表格的功能。

關於在線表格的內容,我以前寫過一篇 Chat,建議閱讀,一樣是免費的。連接:如何快速將線下表格數據線上化

這裏我重點介紹下,後臺數據如何產出?首先要明白,數據確定從數據庫中得到,咱們編寫數據庫 API 的時候,就應該將對應的方法封裝好,前臺經過訪問數據路由,路由調用對應的數據庫 API 便可得到數據,並返回前臺。

下面看下路由的代碼示例:

// api
const studentHandler = require('../../db/handler/studentHandler.js');
// 當前學生列表
router.get('/list', function(req, res, next) {
    let state = 0 ; // 表明當前在校
    studentHandler.getStudentList(state, function(p){
        res.send(p);
    });
});

對應的數據 API 封裝在了 db/handler 目錄中,路由經過調用 getStudentList 方法,便可得到對應的學生列表,並經過 res.send(p) 方法返回給前臺。

再來看看數據庫 API 是如何編寫的?代碼示例以下:

const { Student } = require('../relation.js');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
module.exports = {
    // 根據狀態查詢學生列表
    getStudentList: function(state, callback){
        Student.findAll({
            where: {
                state: state
            }
        }).then(function(data){
            callback(data);
        }).catch(function(err){
            callback(err);
        });
    },
    // 批量添加學生
    uploadStudent: function(data, callback){
        Student.bulkCreate(data).then(function(){
            callback();
        }).catch(function(err){
            callback(err);
        });
    }
};

重點看下 module.exports 對外的接口,篇幅緣由,這裏只提供了兩個調用方法。若是這裏的代碼看不明白,請翻回去查看「使用 ORM 框架」這節內容中,推薦的那篇 Chat。

一般狀況下,大部分功能都是經過「後臺傳參」以及「異步獲取」這兩種方式配合來實現的。

有時候甚至是取代關係,你可能會發現有些後臺管理系統,進去以後,從始至終 URL 就沒有變過,這一類的後臺管理系統的功能實現,所有采用前臺異步獲取的方式。我本人不太推薦這種方式,由於想要單獨展示某個頁面的時候,就很是的尷尬,同時也很是不利於頁面維護。

線上部署

當項目開發完成,本地測試完畢以後。下一步,就是部署上線了。上線部署本來是個比較繁瑣的工做流程,特別針對多系統聯動的狀況。這裏咱們不考慮複雜的狀況,單純講獨立開發的後臺管理系統如何快速部署到線上?

你可能會說,這有啥好講的,把本地代碼同步到服務器上,而後在服務器上啓動不就得了。沒錯,大致是這麼個意思,可是其中仍是有一些點須要注意的。

使用進程管理工具啓動項目

這裏我推薦使用 supervisor,也沒其餘緣由,順手而已。它是一個進程管理工具,當線上的 Web 應用崩潰的時候,它能夠幫助你從新啓動應用,讓應用一直保持線上狀態。

安裝方法很簡單,在命令行中,輸入 npm install -g supervisor 便可。安裝完成以後,咱們須要修改一下項目的啓動命令,打開 package.json 文件,編輯以下:

...
"scripts": {
  "start": "supervisor --harmony -i views/,public/ ./bin/www",
},
...

supervisor --help 查看使用方式,以上命令,配置了 --harmony 模式啓動 Node ,同時,使用 -i 參數忽略了 views/ 以及 public/ 目錄。

修改完成後,咱們依然使用 npm start 啓動項目,不同的是,當咱們修改了除 views/ 以及 public/ 目錄之外的文件後,服務將會自動重啓,以確保線上一直運行的是最新版項目。

使用 Git 同步代碼

將代碼從本地拷貝到線上,有不少種辦法。這裏推薦使用 Git 工具,若是沒有本身的 Git 服務器,那麼就使用 GitHub 類公共 Git 服務平臺。大可沒必要擔憂代碼泄露的問題,GitHub 不是已經提供私有倉庫免費的功能了嘛,沒事,放心用吧。

具體操做我就再也不贅述了。

使用 Nginx 反向代理服務器

爲了確保性能,不建議直接在線上環境,經過 npm 或 supervisor 直接啓動項目,雖然 node 自己的性能並不差,建議仍是在 node 服務中間,再加一層 Web 服務器看成方向代理,與 Node 最配的固然是 Nginx 了。

安裝 Nginx 沒必要多說,下面貼出 Nginx 對應的配置文件內容供參考。

server {
    listen       80;
    server_name xxx.com

    charset utf-8;

    #此處配置你的訪問日誌,請手動建立該目錄:
    access_log  /var/log/nginx/js/access.log;

    location / {
        try_files /_not_exists_ @backend;
    }

    # 這裏爲具體的服務代理配置
    location @backend {
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header Host            $http_host;
        proxy_set_header   X-Forwarded-Proto $scheme;

        #此處配置程序的地址和端口號
        proxy_pass http://127.0.0.1:8080;
    }
}

關鍵記住最後一句:proxy_pass 代理的是哪一個程序地址和端口號。

最後,啓動 Node 服務,啓動 Nginx 服務,而後訪問對應服務地址,大功告成。

總結

這篇 Chat 從 5 個方面講述了全棧開發一個後臺管理系統的大體流程以及部分實戰內容。因爲內容量遠超出了預期,在寫的過程當中,不斷的刪減了不少篇幅。使得 Chat 的總體效果,沒有達到個人預期。

也許這個內容,經過一篇 Chat 確實很難容下吧。因此,我計劃寫完整個課程,內容包括 Node 基礎、進階應用以及Web 實戰等。從最基本的原理開始介紹,到最後全棧開發實戰。

課程以開源免費的形式公開,GitHub 倉庫地址:Node 全棧開發入門課程,更新頻率不定,歡迎關注。

同時,也能夠關注個人微信公衆號:我的學習,以便了解最新進展。

謝謝觀看~

相關文章
相關標籤/搜索