優(yōu)雅的SpringMvc+Mybatis應(yīng)用(四)
轉(zhuǎn)眼間文章已經(jīng)到了第四期了。堅(jiān)持做一件事,確實(shí)是很難的。特別是要不斷的轉(zhuǎn)換思維,一個(gè)習(xí)慣前端開發(fā)的人,做什么還是前端的考慮的多一點(diǎn),后端的架構(gòu)設(shè)計(jì)之類的,現(xiàn)在還談不上,一切穩(wěn)穩(wěn)的前進(jìn)就行了。
關(guān)于上一期,本來是投了首頁的,后來不知道什么原因沒上,檢查了一下,也就是推薦了下自己的博客和github,有點(diǎn)惆悵。
工具
- IDE為idea15
- JDK環(huán)境為1.8
- maven版本為maven3
- Mysql版本為5.5.27
- Tomcat版本為7.0.52
- 流程圖繪制(xmind)
本期目標(biāo)
- 登錄注冊(cè)的簡(jiǎn)單體驗(yàn)優(yōu)化
- 完整的后臺(tái)主頁
- 前端使用json數(shù)據(jù)
- 列表數(shù)據(jù)分頁
注冊(cè)登錄的簡(jiǎn)單體驗(yàn)優(yōu)化
上一期我們注冊(cè)登錄都成功了,但是后臺(tái)主頁顯示很丑陋,所以這里我換了個(gè)主頁,但是前面沒有注意到的細(xì)節(jié)又看到了。如下圖:

在上面的圖中,地址欄顯示的地址是前面注冊(cè)接口的地址,并不是我們常規(guī)看到的xxx/home這種主頁地址。所以我們需要進(jìn)行優(yōu)化處理。
同時(shí),我們可以看到我們的Form表單提交的提示信息是在新產(chǎn)生的ModelAndView界面里面addObject("字段名",數(shù)據(jù)),這樣我們的數(shù)據(jù)都顯示到新的界面去了。也就是說前面的設(shè)計(jì)不合符現(xiàn)在主流的開發(fā)思路,用戶體驗(yàn)也相對(duì)糟糕。我們需要做到在登錄界面前端效驗(yàn)數(shù)據(jù),同時(shí)登錄注冊(cè)的提示信息也是在對(duì)應(yīng)的界面完成的。如下:

值得注意的是:為了程序執(zhí)行效率、數(shù)據(jù)完整性和程序健壯性,我們的前端必須做對(duì)應(yīng)的基礎(chǔ)數(shù)據(jù)效驗(yàn),后端的控制器必須做所有需要的數(shù)據(jù)的效驗(yàn)。
- 前端數(shù)據(jù)效驗(yàn)我們使用js完成
- 前端界面樣式是由CSS完成
- 網(wǎng)絡(luò)請(qǐng)求采用異步請(qǐng)求,具體的實(shí)現(xiàn)是使用的ajax完成
- js獲取web頁面數(shù)據(jù)統(tǒng)一使用標(biāo)簽的ID,格式為:$("#標(biāo)簽ID")
- web頁面標(biāo)簽最好一個(gè)標(biāo)簽一行,這樣代碼看起來更加舒服
我們先重構(gòu)登錄頁面:
首先,我不擅長(zhǎng)寫web頁面,我能做的也就是少量的修改,CSS和js本身不是我的強(qiáng)項(xiàng),需要大量的時(shí)間來磨合。
所以,我選擇了在網(wǎng)上找web界面,然后自己做少量的修改,同時(shí)一些簡(jiǎn)單的小控件我也從網(wǎng)絡(luò)獲取資源來解決需要,合理的查找資源是最快的學(xué)習(xí)方法。
登錄頁面重構(gòu)目標(biāo):
- web前端完成基本的數(shù)據(jù)效驗(yàn)
- 數(shù)據(jù)效驗(yàn)完成后,有基本的對(duì)應(yīng)提示。如上面登錄界面的小標(biāo)簽。
- 異步登錄
- 后端接口返回?cái)?shù)據(jù)為json
- 前端頁面解析json控制程序流轉(zhuǎn)
首先,按照上面的提示,我們可以知道的是前端頁面上面的基本數(shù)據(jù)效驗(yàn)是要使用js完成的,同時(shí)js中獲取web頁面標(biāo)簽的數(shù)據(jù)是需要使用標(biāo)簽的ID完成,簡(jiǎn)單的示例如下:
<script type="text/javascript">
function webLogin() { //定義一個(gè)名為webLogin的js函數(shù)(在java中我們稱呼函數(shù)為方法)
var loginname = $("#u").val();
//var是申明一個(gè)變量的關(guān)鍵字,loginname為變量名,
//$("#u")是找到一個(gè)標(biāo)簽ID為"u"的標(biāo)簽,.val() 是獲取對(duì)應(yīng)ID標(biāo)簽的值
if ("" == loginname) { //u標(biāo)簽的值為空
//只有通過 $("#u") 的形式才能獲取一個(gè)標(biāo)簽。
$("#u").tips({ // .tips 是js提示標(biāo)簽的調(diào)用方法,具體的輪廓如上面的登陸頁面的提示標(biāo)簽
side: 2,
msg: '用戶名不得為空', //提示的信息
bg: '#AE81FF', //背景色
time: 3 //呈現(xiàn)的時(shí)間
});
$("#u").focus(); //讓u標(biāo)簽獲取輸入焦點(diǎn)
return false; //返回false,打斷js的執(zhí)行
}
var loginpwd = $("#p").val();
if (loginpwd == "") {
$("#p").tips({
side: 2,
msg: '密碼不得為空',
bg: '#AE81FF',
time: 3
});
$("#p").focus();
return false;
}
$.ajax({ //使用jquery下面的ajax開啟網(wǎng)絡(luò)請(qǐng)求
type: "POST", //http請(qǐng)求方式為POST
url: '<%=request.getContextPath()%>/userAction/login', //請(qǐng)求接口的地址
data: {loginId: loginname, pwd: loginpwd}, //存放的數(shù)據(jù),服務(wù)器接口字段為loginId和pwd,分別對(duì)應(yīng)用戶登錄名和密碼
dataType: 'json', //當(dāng)這里指定為json的時(shí)候,獲取到了數(shù)據(jù)后會(huì)自動(dòng)解析的,只需要 返回值.字段名稱 就能使用了
cache: false, //不用緩存
success: function (data) { //請(qǐng)求成功,http狀態(tài)碼為200。返回的數(shù)據(jù)已經(jīng)打包在data中了。
if (data.code == 1) { //獲判斷json數(shù)據(jù)中的code是否為1,登錄的用戶名和密碼匹配,通過效驗(yàn),登陸成功
window.location.href = "<%=request.getContextPath()%>/mvc/home"; //跳轉(zhuǎn)到主頁
} else { //登錄不成功
alert(data.msg); //彈出對(duì)話框,提示返回的錯(cuò)誤信息
$("#u").focus();
}
}
});
}
</script>
上面的注釋已經(jīng)能很明顯的看出我們的 前端效驗(yàn)、網(wǎng)絡(luò)請(qǐng)求和js解析json,下面我們?cè)谇岸隧撁嬷姓{(diào)用這個(gè)js,如下:
<form action="" //此處必須刪掉form表單的地址
name="loginform"
accept-charset="utf-8"
id="login_form"
class="loginForm"
method="post">
<input type="hidden" name="did" value="0"/>
<input type="hidden" name="to" value="log"/>
<div class="uinArea" id="uinArea">
<label class="input-tips" for="u">帳號(hào):</label>
<div class="inputOuter" id="uArea">
<input type="text" id="u" name="loginId" class="inputstyle"/>
</div>
</div>
<div class="pwdArea" id="pwdArea">
<label class="input-tips" for="p">密碼:</label>
<div class="inputOuter" id="pArea">
<input type="password" id="p" name="pwd" class="inputstyle"/>
</div>
</div>
<div style="padding-left:50px;margin-top:20px;">
<input type="button"
id="btn_login"
value="登 錄"
onclick="webLogin();" //此處調(diào)用我們上面寫的js的登錄方法
style="width:150px;"
class="button_blue"/>
</div>
</form>
上面就是web中調(diào)用js的簡(jiǎn)單實(shí)現(xiàn),注意的是,FORM表單必須刪除action的值,在點(diǎn)擊后需要觸發(fā)對(duì)應(yīng)事件的地方調(diào)用js。
當(dāng)然,我們的前端頁面完成后,我們必須在后端接口處,做出對(duì)應(yīng)的修改,讓他符合我們前端的調(diào)用規(guī)則。后端修改如下:
/**
* 用戶請(qǐng)求相關(guān)控制器
* <br/>Created by acheng on 2016/9/26.
*/
@Controller //標(biāo)明本類是控制器
@RequestMapping("/userAction") //外層地址
public class UserController {
@Autowired
private UserServiceImpl userService; //自動(dòng)載入Service對(duì)象
private ResponseObj responseObj; //返回json數(shù)據(jù)的實(shí)體
/**
* 登錄接口,因?yàn)閖son數(shù)據(jù)外層一般都是Object類型,所以返回值必須是Object<br/>
* 這里的地址是: 域名/userAction/login
*
* @param req
* @param user
* @return
*/
@RequestMapping(value = "/login" //內(nèi)層地址
, method = RequestMethod.POST //限定請(qǐng)求方式
, produces = "application/json; charset=utf-8") //設(shè)置返回值是json數(shù)據(jù)類型
@ResponseBody
public Object login(HttpServletRequest req, User user) {
Object result;
if (null == user) {
responseObj = new ResponseObj<User>();
responseObj.setCode(ResponseObj.EMPUTY);
responseObj.setMsg("登錄信息不能為空");
result = new GsonUtils().toJson(responseObj); //通過gson把java bean轉(zhuǎn)換為json
return result; //返回json
}
if (StringUtils.isEmpty(user.getLoginId()) || StringUtils.isEmpty(user.getPwd())) {
responseObj = new ResponseObj<User>();
responseObj.setCode(ResponseObj.FAILED);
responseObj.setMsg("用戶名或密碼不能為空");
result = new GsonUtils().toJson(responseObj);
return result;
}
//查找用戶
User user1 = userService.findUser(user);
if (null == user1) {
responseObj = new ResponseObj<User>();
responseObj.setCode(ResponseObj.EMPUTY);
responseObj.setMsg("未找到該用戶");
result = new GsonUtils().toJson(responseObj);
} else {
if (user.getPwd().equals(user1.getPwd())) {
responseObj = new ResponseObj<User>();
responseObj.setCode(ResponseObj.OK); //登錄成功,狀態(tài)為1
responseObj.setMsg(ResponseObj.OK_STR);
responseObj.setData(user1); //登陸成功后返回用戶信息
result = new GsonUtils().toJson(responseObj);
} else {
responseObj = new ResponseObj<User>();
responseObj.setCode(ResponseObj.FAILED);
responseObj.setMsg("用戶密碼錯(cuò)誤");
result = new GsonUtils().toJson(responseObj);
}
}
return result;
}
}
注意:如果為了返回?cái)?shù)據(jù)為json,那么我們需要設(shè)定某個(gè)方法對(duì)應(yīng)的注解為:@ResponseBody 。 否則會(huì)產(chǎn)生404錯(cuò)誤!
我們通過上面的重構(gòu)可以明白以下幾點(diǎn):
- 前端
- js實(shí)現(xiàn)基本的數(shù)據(jù)效驗(yàn)
- js發(fā)起網(wǎng)絡(luò)請(qǐng)求
- ajax發(fā)起網(wǎng)絡(luò)請(qǐng)求,返回類型設(shè)置json能自動(dòng)解析
- js獲取頁面控件
- 頁面控件調(diào)用js
- js獲取解析后的json數(shù)據(jù)的值,進(jìn)行程序流轉(zhuǎn)控制
- 后端:
- 后端控制器必須申明
- 后端的地址必須配置
- 每個(gè)地址返回的數(shù)據(jù)類型要匹配
- 返回json數(shù)據(jù),方法上面必須配置:@ResponseBody
- 可以使用工具類來方便開發(fā)
后臺(tái)主頁→個(gè)人信息修改
上期我們可以看到,我們的登錄和注冊(cè)都是已經(jīng)OK了?,F(xiàn)在我們登錄和注冊(cè)成功后,我們都讓他跳轉(zhuǎn)到主頁去。同時(shí)完善登錄和注冊(cè)的錯(cuò)誤提示頁面。
一般來說,大家更喜歡看到登錄成功后的主頁界面,畢竟大多數(shù)人都是有喜新厭舊之嫌。我也是一樣的。哈哈。
為了程序的執(zhí)行邏輯,考慮后端需求都不是那么單一,我們先做一些公共的建設(shè)。比如說用戶信息修改現(xiàn)實(shí)之類的。如下圖:

如上圖所示,我們需要一個(gè)可以彈出的對(duì)話框,我去百度了一下,那個(gè)“妹子UI”還是很受人歡迎,所以就集成進(jìn)來了。
我們選取一個(gè)比較喜歡的后端主頁,然后把對(duì)應(yīng)的資源放入到對(duì)應(yīng)的文件目錄(js、css、images等),需要新加入的資源如果在以前的目錄中沒有的話,那么我們需要在里面進(jìn)行配置。比如說這里我加入了字體文件,那么我現(xiàn)在需要先把字體文件指定目錄為:
static/font/
目錄指定后我需要在Spring的配置文件,spring-web.xml中配置靜態(tài)資源的目錄如下:
<mvc:resources mapping="/fonts/**" location="/static/fonts/"/>
剩下的就是寫好jsp頁面(Copy+Pause+修改資源文件路徑)。然后我們?cè)贑ontroller中配置好路徑
/**
* 后臺(tái)主頁
*
* @return
*/
@RequestMapping(value = "/home", method = RequestMethod.GET)
public String home() {
return "home";
}
這樣子配置好了后,我們就可以直接用“域名/mvc/home”來訪問我們的主頁了。同時(shí)按照上面的設(shè)置,我們登錄成功后,直接解析json確認(rèn)用戶登錄成功,然后前端使用js來進(jìn)行頁面跳轉(zhuǎn),如:
window.location.href = "<%=request.getContextPath()%>/mvc/home"; //跳轉(zhuǎn)到主頁
這樣,我們就能修復(fù)上面說道的頁面和地址顯示不匹配的問題。
同時(shí),通過上面的可以看出,我們?cè)趈sp頁面中,純粹沒加入任何jsva代碼,全是使用的前端+接口實(shí)現(xiàn)的功能。我們這樣做,以后維護(hù)和重構(gòu)中也能降低一部分壓力。
言歸正傳,我們這里主要是想做一個(gè)個(gè)人信息修改的功能。首先我們進(jìn)行功能和業(yè)務(wù)流程分析。
功能和業(yè)務(wù)流程分析:
- 1.web點(diǎn)擊頭像顯示修改信息對(duì)話框。
- 2.根據(jù)后端定義的用戶信息表,得出用戶信息修改需要填寫的資料。
- 3.用戶上傳個(gè)人資料,上傳之前前端必須先進(jìn)行基礎(chǔ)信息驗(yàn)證。
- 4.用戶個(gè)人信息驗(yàn)證通過后,上傳到服務(wù)器。(重點(diǎn))
- 5.服務(wù)器接收上傳的信息,進(jìn)行存儲(chǔ),并返回修改結(jié)果。(重點(diǎn))
從上面我們可以看到我畫出兩個(gè)重點(diǎn),而且這兩個(gè)重點(diǎn)都是java web避免不了事情。為什么這樣說呢?
- 1.任何一個(gè)動(dòng)態(tài)的web服務(wù)器都免不了數(shù)據(jù)資料的更新,數(shù)據(jù)資料更新一般分為兩種。
- 有文件的信息上傳
- 無文件的信息上傳
- 2.可能其他童鞋看到http請(qǐng)求的方法有很多種,但是一般來說get和post我們能做出任何的操作。
- 3.在大量數(shù)據(jù)的服務(wù)器中,考慮到很多因素(歷史記錄查詢、數(shù)據(jù)庫增量等),一般不會(huì)進(jìn)行真正的物理數(shù)據(jù)刪除,一般都是通過控制輸出來實(shí)現(xiàn)的。(實(shí)戰(zhàn)經(jīng)驗(yàn),血淚教訓(xùn),切記)
現(xiàn)在我們開始實(shí)現(xiàn)對(duì)話框:
打開“妹子UI”的js插件頁面,我們找到模態(tài)窗口相關(guān)的文檔,在“模擬 Prompt”這里,我們可以看到具體的對(duì)話框的實(shí)現(xiàn)和調(diào)用如下:
<!--這是html代碼-->
<button
type="button"
class="am-btn am-btn-success"
id="doc-prompt-toggle">
Prompt
</button>
<div class="am-modal am-modal-prompt" tabindex="-1" id="my-prompt">
<div class="am-modal-dialog">
<div class="am-modal-hd">Amaze UI</div>
<div class="am-modal-bd">
來來來,吐槽點(diǎn)啥吧
<input type="text" class="am-modal-prompt-input">
</div>
<div class="am-modal-footer">
<span class="am-modal-btn" data-am-modal-cancel>取消</span>
<span class="am-modal-btn" data-am-modal-confirm>提交</span>
</div>
</div>
</div>
<!--這是js調(diào)用-->
$(function() {
$('#doc-prompt-toggle').on('click', function() { //在這里設(shè)定上面的按鈕的點(diǎn)擊函數(shù)
$('#my-prompt').modal({ //顯示ID為my-prompt的窗口
relatedTarget: this,
onConfirm: function(e) { //窗口的確定按鈕的響應(yīng)事件
alert('你輸入的是:' + e.data || '')
},
onCancel: function(e) { //取消按鈕的響應(yīng)事件
alert('不想說!');
}
});
});
});
關(guān)于上面的相關(guān)代碼,我們需要引入妹子UI后才能使用!?。〗酉聛砦覀冃枰脑斐煞衔覀儗?shí)際需求的界面,如下:
<!--這里是html代碼-->
<div class="am-modal am-modal-prompt" tabindex="-1" id="my-prompt">
<div class="am-modal-dialog">
<div class="am-modal-hd">用戶信息修改</div>
<div class="am-modal-bd">
<form enctype="multipart/form-data" accept-charset="UTF-8">
姓名:
<input type="text" class="am-modal-prompt-input" id="changeName">
性別:
<input type="text" class="am-modal-prompt-input" id="changeSex">
手機(jī)號(hào):
<input type="text" class="am-modal-prompt-input" id="changeCell">
年齡:
<input type="text" class="am-modal-prompt-input" id="changeAge">
頭像:
<div class="am-modal-prompt-input">
<input type="file" name="file"
id="changeHeadPic" size="28"/>
</div>
</form>
</div>
<div class="am-modal-footer">
<span class="am-modal-btn" data-am-modal-cancel>取消</span>
<span class="am-modal-btn" data-am-modal-confirm>上傳</span>
</div>
</div>
</div>
<!--下面是js代碼-->
var fileName;
<!--文件上傳這里加入了js插件:jquery.ajaxfileupload.js-->
function uploadFile() {
//這里應(yīng)該加入Loading 窗口開啟
fileName = document.getElementById('changeHeadPic').value;
$.ajaxFileUpload({
url: "<%=request.getContextPath()%>/userAction/uploadHeadPic",
secureuri: false, //是否需要安全協(xié)議,一般設(shè)置為false
fileElementId: 'changeHeadPic', //文件上傳域的ID
dataType: 'json', //返回值類型 一般設(shè)置為json
contentType: "application/x-www-form-urlencoded; charset=utf-8",
success: function (data) {
alert(data.msg);
//先根據(jù)返回的code確定文件是否上傳成功
//文件上傳失敗,直接彈出錯(cuò)誤提示,根據(jù)錯(cuò)誤進(jìn)行相應(yīng)的事物處理(關(guān)閉Loading窗口,彈出提示對(duì)話框)
//文件上傳成功后,繼續(xù)現(xiàn)實(shí)loading窗口,接著執(zhí)行上傳表單信息等事物
}
});
}
function changeUserInfo() { //顯示個(gè)人信息修改窗口
$('#my-prompt').modal({
relatedTarget: this,
onConfirm: function () {
uploadFile(); //調(diào)用上面的文件上傳函數(shù)
},
onCancel: function (e) {
}
});
}
上面的代碼,我們完成了控制窗口顯示的函數(shù),完成了修改個(gè)人信息界面的構(gòu)建。現(xiàn)在我們需要的是找到執(zhí)行程序入口。按照我的習(xí)慣,肯定是找到頭像控件,然后設(shè)置點(diǎn)擊事件為上面的changeUserInfo()。實(shí)現(xiàn)如下:
<!--下面是頭像的html代碼,在頭像控件后面的點(diǎn)擊事件上面添加上函數(shù)就行了。-->
<div class="user-img">
<img src="/static/images/avatar-1.jpg" alt="user-img" title="Mat Helme"
class="img-circle img-thumbnail img-responsive" onclick="changeUserInfo()"> <!--在這里添加onclick方法的值為:changeUserInfo()-->
<div class="user-status offline">
<i class="am-icon-dot-circle-o" aria-hidden="true"></i>
</div>
</div>
好的,上面我們可以看到我的前端界面代碼基本上完成了。接下來,我們需要在我們后端上面寫上對(duì)應(yīng)的程序接口,實(shí)現(xiàn)功能即可。
本來計(jì)劃文件上傳單獨(dú)使用commons-fileupload和commons-io完成的,畢竟這是在Servelt上面的老套路,但是我發(fā)現(xiàn)Spring里面已經(jīng)考慮到這一點(diǎn),有新的東西來完成,所以就使用了Spring的實(shí)現(xiàn)方式。具體代碼如下:
//我們?cè)赨serController這個(gè)控制器里添加這個(gè)方法
@RequestMapping(value = "/uploadHeadPic"
, method = RequestMethod.POST
, produces = "application/json; charset=utf-8")
@ResponseBody
public Object uploadHeadPic(@RequestParam(required = false) MultipartFile file, HttpServletRequest request) {
//在這里面文件存儲(chǔ)的方案一般是:收到文件→獲取文件名→在本地存儲(chǔ)目錄建立防重名文件→寫入文件→返回成功信息
//如果上面的步驟中在結(jié)束前任意一步失敗,那就直接失敗了。
if (null == file || file.isEmpty()) {
responseObj = new ResponseObj();
responseObj.setCode(ResponseObj.FAILED);
responseObj.setMsg("文件不能為空");
return new GsonUtils().toJson(responseObj);
}
responseObj = new ResponseObj();
responseObj.setCode(ResponseObj.OK);
responseObj.setMsg("文件長(zhǎng)度為:" + file.getSize());
return new GsonUtils().toJson(responseObj);
}
完成了上面的方法后,我們覺得應(yīng)該是沒問題了,畢竟這樣一個(gè)接口來接受請(qǐng)求是沒問題的嘛,是的,我也是這么認(rèn)為的。
但是現(xiàn)實(shí)的打臉是很嚴(yán)重的,因?yàn)榘凑者@么寫后,我無論如何都收不到文件(文件一直為null),Why?我的思路是正確的啊。經(jīng)過我的仔細(xì)查找,發(fā)現(xiàn)我的Spring的配置文件中,沒有添加文件的支持設(shè)置,所以我們又得補(bǔ)充配置文件,spring-web.xml新增配置如下:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--下面設(shè)置的是上傳文件的最大大小-->
<property name="maxUploadSize" value="10000000"/>
</bean>
經(jīng)過上面的一番折騰,我們發(fā)現(xiàn)框架這個(gè)東西,也不是一勞永逸的,畢竟很多東西需要不斷的增加。
總結(jié):
- 任何東西都需要根據(jù)需求不斷的變化,可增可減,張弛有度。
- Spring接收文件上傳時(shí),Controller的具體方法的參數(shù)前面插入注解,同時(shí)數(shù)據(jù)類型是MultipartFile。
- 引入第三方資源的時(shí)候,必須查看文檔,根據(jù)說明文檔好辦事。
- js進(jìn)行前端流程控制,后端接口隔離,前后端解耦。
前兩天刮臺(tái)風(fēng),停電導(dǎo)致數(shù)據(jù)丟失,是個(gè)很尷尬的事情,拖慢了進(jìn)度。同時(shí),朋友遇到點(diǎn)問題,我在開導(dǎo)他。文章寫到現(xiàn)在也是凌晨4點(diǎn)過了,本期計(jì)劃的列表分頁也沒做,很對(duì)不起大家對(duì)我的期待。真誠(chéng)的說一聲:對(duì)不起。對(duì)不起你們的期待。
給朋友開導(dǎo)的時(shí)候,我也總結(jié)了下做人做事:隨心、追夢(mèng)、勇敢、獨(dú)行。希望有心做事的,都用這幾句話勉勵(lì)自己吧。
前行的路上不只是孤獨(dú),還有滿山的鮮花,更有遠(yuǎn)方和詩。
下期預(yù)告:
- 列表分頁
- 簡(jiǎn)易用戶角色控制
- 攔截器的使用