管理系統(tǒng):從數(shù)據(jù)庫(kù)設(shè)計(jì)到前后臺(tái)部署全解析)
做JavaWeb課程設(shè)計(jì)或者畢業(yè)設(shè)計(jì)的同學(xué)十個(gè)里有八個(gè)都繞不過(guò)“圖書(shū)管理系統(tǒng)”這個(gè)題目。而當(dāng)你拿到的需求是“基于javaweb和mysql的ssm前臺(tái)后臺(tái)精品圖書(shū)管理系統(tǒng)(javassmjspmysql)”時(shí)本質(zhì)上你要交付的不只是一堆能跑的頁(yè)面而是一個(gè)把前臺(tái)用戶操作和后臺(tái)管理維護(hù)打通了的完整項(xiàng)目。這套東西放在簡(jiǎn)歷上是SSM整合經(jīng)驗(yàn)的證明放在答辯現(xiàn)場(chǎng)是數(shù)據(jù)庫(kù)設(shè)計(jì)和業(yè)務(wù)邏輯的展示。我將從怎么拆需求、怎么搭環(huán)境、怎么設(shè)計(jì)表、怎么寫(xiě)核心代碼再到上線部署前必須處理掉的坑完整走一遍這個(gè)項(xiàng)目的落地路徑。不管你是剛學(xué)完JavaWeb基礎(chǔ)、準(zhǔn)備做課程設(shè)計(jì)的新手還是被畢設(shè)題目折騰到到處找參考的在校生或者接了外包單子需要快速理解需求的開(kāi)發(fā)這份經(jīng)驗(yàn)都能幫你少走一半彎路。1. 拆解“前臺(tái)后臺(tái)”這套圖書(shū)管理系統(tǒng)的業(yè)務(wù)邊界到底在哪對(duì)著“前臺(tái)后臺(tái)”這幾個(gè)字很多人第一反應(yīng)是“多做幾個(gè)頁(yè)面就行”。真上手才發(fā)現(xiàn)前臺(tái)的用戶頁(yè)和后臺(tái)的管理頁(yè)之間數(shù)據(jù)是同一套但操作邏輯和權(quán)限邊界完全不一樣。先把業(yè)務(wù)邊界理清楚后面寫(xiě)代碼才不會(huì)寫(xiě)著寫(xiě)著就亂套。1.1 前臺(tái)功能用戶能做什么前臺(tái)的典型用戶是“借書(shū)人”不是管理員。圍繞“找書(shū)—借書(shū)—還書(shū)—看自己借了啥”這條主鏈路前臺(tái)至少要覆蓋這幾個(gè)場(chǎng)景注冊(cè)與登錄用戶能自己注冊(cè)賬號(hào)密碼不能明文存數(shù)據(jù)庫(kù)登錄后狀態(tài)要能保持。圖書(shū)瀏覽與檢索按分類(lèi)瀏覽圖書(shū)按書(shū)名、作者、ISBN做模糊搜索結(jié)果要分頁(yè)展示。圖書(shū)詳情頁(yè)能看到封面、簡(jiǎn)介、庫(kù)存量、價(jià)格等基本信息。借書(shū)操作點(diǎn)擊借閱時(shí)系統(tǒng)要檢查這本書(shū)還有沒(méi)有庫(kù)存、這個(gè)用戶有沒(méi)有逾期未還的圖書(shū)記錄。我的借閱展示當(dāng)前用戶的借閱列表區(qū)分“借閱中”和“已歸還”還書(shū)操作也在這里觸發(fā)。個(gè)人資料修改手機(jī)號(hào)、密碼等基本信息。實(shí)際開(kāi)發(fā)中前臺(tái)頁(yè)面大概就是首頁(yè)、圖書(shū)列表頁(yè)、詳情頁(yè)、登錄注冊(cè)頁(yè)、個(gè)人中心這五六個(gè)頁(yè)面??雌饋?lái)不多但每個(gè)頁(yè)面對(duì)應(yīng)的Controller、Service、Mapper都要完整走一遍。1.2 后臺(tái)功能管理員能做什么后臺(tái)是給管理員用的核心就一個(gè)詞維護(hù)。管理員不關(guān)心“這本書(shū)該不該借”只關(guān)心“數(shù)據(jù)是否準(zhǔn)確、有沒(méi)有人欠書(shū)不還”。后臺(tái)模塊一般拆成這幾塊圖書(shū)管理圖書(shū)的增刪改查、上下架、封面圖上傳。用戶管理查看注冊(cè)用戶列表、禁用違規(guī)賬號(hào)。借閱管理查看所有人的借閱記錄、處理還書(shū)、標(biāo)記逾期。分類(lèi)管理維護(hù)圖書(shū)分類(lèi)新增或刪除。公告管理發(fā)布簡(jiǎn)單的系統(tǒng)公告前臺(tái)首頁(yè)可以展示。后臺(tái)和前臺(tái)最大的區(qū)別不是頁(yè)面長(zhǎng)什么樣而是操作權(quán)限。后臺(tái)URL必須經(jīng)過(guò)管理員校驗(yàn)不能通過(guò)前臺(tái)普通用戶身份訪問(wèn)。用SSM框架實(shí)現(xiàn)這種權(quán)限分流核心就是攔截器加Session角色判斷這部分后面單獨(dú)展開(kāi)。1.3 為什么這個(gè)題目會(huì)選SSM而不是Spring Boot這幾乎是面試必問(wèn)的問(wèn)題“你怎么看待SSM和Spring Boot的區(qū)別”對(duì)于課設(shè)和畢設(shè)來(lái)說(shuō)選SSM其實(shí)是刻意為之的。SSM的每個(gè)組件職責(zé)非常清晰——Spring管對(duì)象、SpringMVC管請(qǐng)求分發(fā)、MyBatis管數(shù)據(jù)庫(kù)操作三者耦合度低代碼走到哪一步一看便知。相比之下Spring Boot雖然起步快但很多配置被自動(dòng)裝配“藏”起來(lái)了評(píng)審老師追問(wèn)底層機(jī)制時(shí)初學(xué)者反而容易答不上來(lái)。SSM這套架構(gòu)下一個(gè)典型的請(qǐng)求路徑是這樣的JSP頁(yè)面發(fā)起請(qǐng)求 - SpringMVC的DispatcherServlet - Controller - Service接口實(shí)現(xiàn) - Mapper接口 - MyBatis執(zhí)行SQL - MySQL返回結(jié)果把這條鏈路在腦子里跑通整個(gè)SSM項(xiàng)目你就理解了八成。后面所有功能都是往這條鏈路上掛新接口而已。2. 環(huán)境與工程骨架從零把SSMJSPMySQL在IDEA里跑起來(lái)很多同學(xué)項(xiàng)目代碼寫(xiě)得差不多了卻卡在“項(xiàng)目起不來(lái)”這一步。IDEA跑JavaWeb項(xiàng)目這件事看似簡(jiǎn)單實(shí)際操作里的坑一個(gè)接一個(gè)——Tomcat版本不匹配、依賴沒(méi)引全、數(shù)據(jù)庫(kù)驅(qū)動(dòng)版本不對(duì)、SSL連接報(bào)錯(cuò)……我把整個(gè)環(huán)境搭建過(guò)程重新梳理了一遍按順序走基本不會(huì)出問(wèn)題。2.1 IDEA配置Tomcat并運(yùn)行JavaWeb項(xiàng)目的關(guān)鍵操作如果你是用IDEA打開(kāi)別人給的SSM項(xiàng)目第一步不是點(diǎn)綠色按鈕而是確認(rèn)三件事JDK版本、Tomcat版本、Maven依賴是否全部下載完成。這三件是項(xiàng)目能跑起來(lái)的地基任何一件出問(wèn)題報(bào)錯(cuò)千奇百怪。項(xiàng)目結(jié)構(gòu)里檢查Project SDKSSM項(xiàng)目通常用的JDK 1.8或11用17容易遇到Tomcat版本不適配和各種反射警告。確認(rèn)Libararies里是否已有Spring、SpringMVC、MyBatis、MySQL驅(qū)動(dòng)等依賴。如果用的是Maven管理執(zhí)行clean reimport看右側(cè)Maven面板的依賴列表是否全部變綠。配置Tomcat時(shí)關(guān)鍵是“Deployment”標(biāo)簽頁(yè)里把項(xiàng)目的war包添加進(jìn)去Application context設(shè)為/。很多人只配了Server標(biāo)簽頁(yè)沒(méi)配Deployment一啟動(dòng)瀏覽器404。如果你拿到的是不帶Maven的老式SSM項(xiàng)目依賴都放在web/WEB-INF/lib下那更要注意Tomcat版本和依賴jar包版本的兼容性。我建議能轉(zhuǎn)Maven就轉(zhuǎn)Maven版本沖突和缺少傳遞依賴的問(wèn)題會(huì)少很多。實(shí)踐中最穩(wěn)的依賴組合是Spring 5.x MyBatis 3.5.x MySQL驅(qū)動(dòng)8.x JSTL 1.2Tomcat選擇8.5或9.0。2.2 配置SSM三個(gè)核心文件applicationContext.xml、spring-mvc.xml、mybatis-config.xmlSSM整合的本質(zhì)是Spring容器把SpringMVC和MyBatis全部管起來(lái)。多數(shù)教學(xué)項(xiàng)目會(huì)拆成三個(gè)配置文件各管一攤applicationContext.xml只管Service層和Mapper層的Bean開(kāi)啟注解掃描時(shí)注意排除掉Controller注解。spring-mvc.xml只管Controller層開(kāi)啟注解驅(qū)動(dòng)、配置視圖解析器InternalResourceViewResolver。mybatis-config.xml配置MyBatis全局屬性比如下劃線轉(zhuǎn)駝峰再交給Spring的SqlSessionFactoryBean加載。新手最常見(jiàn)的問(wèn)題是在applicationContext.xml里掃描Controller的注解在spring-mvc.xml里又掃描了Service結(jié)果導(dǎo)致同一個(gè)Bean被創(chuàng)建兩次有些方法調(diào)用時(shí)拿到的是空的代理對(duì)象啟動(dòng)不報(bào)錯(cuò)一調(diào)用就報(bào)空指針。正確做法是Spring配置文件里掃描service和daoSpringMVC配置文件里只掃描controller。2.3 MySQL 8安裝配置與驅(qū)動(dòng)、SSL連接的坑數(shù)據(jù)庫(kù)環(huán)境的坑比項(xiàng)目代碼更隱蔽?,F(xiàn)在電腦上裝的大概率是MySQL 8.x和很多博客里寫(xiě)的5.x配置有幾點(diǎn)關(guān)鍵區(qū)別JDBC驅(qū)動(dòng)類(lèi)不要再用com.mysql.jdbc.Driver要寫(xiě)com.mysql.cj.jdbc.Driver。JDBC連接URL里最好帶上useSSLfalse和serverTimezoneAsia/Shanghai否則啟動(dòng)時(shí)會(huì)報(bào)SSL連接錯(cuò)誤或時(shí)區(qū)錯(cuò)誤這是MySQL 8的常見(jiàn)問(wèn)題。一個(gè)典型可用的JDBC配置如下jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456在IDEA里連接數(shù)據(jù)庫(kù)時(shí)如果也遇到SSL報(bào)錯(cuò)直接編輯連接在Driver Properties里把useSSL設(shè)為false即可。別一看到SSL就慌這只是MySQL 8默認(rèn)走安全連接而本地開(kāi)發(fā)環(huán)境證書(shū)不全導(dǎo)致的。配置完環(huán)境我強(qiáng)烈建議先在數(shù)據(jù)庫(kù)客戶端Navicat或IDEA自帶的Database面板里把數(shù)據(jù)庫(kù)和表建好、灌入幾條測(cè)試數(shù)據(jù)再啟動(dòng)項(xiàng)目。否則項(xiàng)目起來(lái)了頁(yè)面上一片空白或者報(bào)“Table doesn’t exist”很難分清是代碼問(wèn)題還是數(shù)據(jù)庫(kù)沒(méi)初始化。最好用Navicat跑一遍全部建表SQL文件確認(rèn)沒(méi)報(bào)錯(cuò)再繼續(xù)。3. 數(shù)據(jù)庫(kù)設(shè)計(jì)圖書(shū)、用戶、借閱、分類(lèi)四張核心表的落地細(xì)節(jié)圖書(shū)管理系統(tǒng)的數(shù)據(jù)庫(kù)設(shè)計(jì)網(wǎng)上能找到很多版本但大多存在一個(gè)通病邏輯上只有圖書(shū)和用戶兩張表借閱關(guān)系直接用一張中間表草草了事。等做到“我的借閱”頁(yè)面需要顯示圖書(shū)名、作者、借閱時(shí)間、應(yīng)還時(shí)間時(shí)就開(kāi)始瘋狂聯(lián)表查詢頁(yè)面越寫(xiě)越痛苦。我的建議是把四張表的關(guān)系一開(kāi)始就定死圖書(shū)表、用戶表、圖書(shū)分類(lèi)表、借閱記錄表。這樣后期功能擴(kuò)展空間大代碼也更好寫(xiě)。3.1 四張表的完整字段設(shè)計(jì)圖書(shū)表book的字段設(shè)計(jì)要考慮前臺(tái)展示和后臺(tái)檢索的需求核心字段如下字段名類(lèi)型說(shuō)明book_idint主鍵自增book_namevarchar(100)書(shū)名authorvarchar(50)作者publishvarchar(100)出版社isbnvarchar(20)ISBN可做唯一索引pricedecimal(10,2)價(jià)格category_idint分類(lèi)ID邏輯外鍵covervarchar(200)封面圖片路徑stockint庫(kù)存數(shù)量descriptiontext簡(jiǎn)介create_timedatetime入庫(kù)時(shí)間用戶表user除了基本信息還必須有一個(gè)status字段來(lái)支持管理員禁用賬號(hào)的操作字段名類(lèi)型說(shuō)明user_idint主鍵自增usernamevarchar(30)用戶名唯一索引passwordvarchar(64)經(jīng)過(guò)MD5加鹽后的密碼real_namevarchar(50)真實(shí)姓名phonevarchar(20)手機(jī)號(hào)statustinyint1正常0禁用create_timedatetime注冊(cè)時(shí)間借閱表是整個(gè)系統(tǒng)的核心它記錄的不只是“誰(shuí)借了哪本書(shū)”還應(yīng)該記錄借出時(shí)間、應(yīng)還時(shí)間、實(shí)際歸還時(shí)間、當(dāng)前狀態(tài)字段名類(lèi)型說(shuō)明borrow_idint主鍵自增user_idint借閱人IDbook_idint圖書(shū)IDborrow_timedatetime借出時(shí)間return_timedatetime應(yīng)還時(shí)間通常借出時(shí)間30天actual_return_timedatetime實(shí)際歸還時(shí)間未還為nullstatustinyint0借閱中1已歸還2逾期未還分類(lèi)表category就簡(jiǎn)單了category_id和category_name兩個(gè)字段前臺(tái)導(dǎo)航欄按分類(lèi)查書(shū)時(shí)用。3.2 為什么借閱表要冗余用戶和圖書(shū)信息這是我和很多同學(xué)看法不一樣的地方。教科書(shū)上總是強(qiáng)調(diào)“范式”數(shù)據(jù)庫(kù)設(shè)計(jì)要盡量消除冗余但實(shí)際做這種業(yè)務(wù)系統(tǒng)時(shí)我傾向于在借閱表里冗余賬號(hào)和書(shū)名兩個(gè)字段。原因是聯(lián)表查詢的性能和維護(hù)成本。如果借閱列表頁(yè)要顯示“誰(shuí)借了哪本書(shū)”按第三范式必須join用戶表和圖書(shū)表。當(dāng)數(shù)據(jù)量到了幾萬(wàn)條借閱記錄每次打開(kāi)后臺(tái)借閱管理頁(yè)面都要做兩次大表關(guān)聯(lián)數(shù)據(jù)庫(kù)壓力很大。更重要的是如果某本書(shū)后來(lái)被管理員刪除了物理外鍵會(huì)導(dǎo)致借閱記錄刪除失敗邏輯外鍵則會(huì)讓借閱記錄變成“孤兒數(shù)據(jù)”頁(yè)面上一顯示就是空白或報(bào)錯(cuò)。所以我在實(shí)際項(xiàng)目中這樣處理借閱表里除了user_id和book_id還冗余了user_name和book_name兩個(gè)字段查詢借閱流水時(shí)不需要聯(lián)表直接查這一張表就夠。前提是系統(tǒng)里用戶名不能改、書(shū)名改了以后歷史借閱記錄顯示的還是舊名字也無(wú)所謂。對(duì)于圖書(shū)管理系統(tǒng)這個(gè)場(chǎng)景這個(gè)取舍完全值得。3.3 字段默認(rèn)值與初始化數(shù)據(jù)的幾個(gè)經(jīng)驗(yàn)建表時(shí)還有一個(gè)容易忽略的細(xì)節(jié)字段默認(rèn)值。比如customer的status字段默認(rèn)設(shè)置成1正常借閱表的status默認(rèn)0借閱中圖書(shū)的stock默認(rèn)0而不是null。從數(shù)據(jù)庫(kù)層面多加一層兜底代碼里就能少寫(xiě)很多空值判斷。初始化數(shù)據(jù)也要提前準(zhǔn)備不要等運(yùn)行時(shí)再手動(dòng)敲??梢詫?xiě)一個(gè)init.sql里面插入幾個(gè)分類(lèi)文學(xué)、科技、歷史、少兒、10本左右圖書(shū)、一個(gè)管理員賬號(hào)admin/admin、一個(gè)測(cè)試用戶。這樣項(xiàng)目第一次啟動(dòng)頁(yè)面上就有內(nèi)容可看前臺(tái)搜索、借閱、后臺(tái)管理都能直接演示答辯的時(shí)候也不會(huì)出現(xiàn)“頁(yè)面空空如也”的尷尬。4. 前后臺(tái)權(quán)限分流登錄校驗(yàn)、攔截器、密碼加鹽一個(gè)都不能少圖書(shū)管理系統(tǒng)的“前后臺(tái)”不僅是頁(yè)面路由的區(qū)分更是安全邊界的區(qū)分。普通用戶JSP頁(yè)面可以訪問(wèn)前臺(tái)但后臺(tái)管理頁(yè)必須管理員才能進(jìn)用戶調(diào)接口必須帶著登錄Session才能操作。用SSM實(shí)現(xiàn)這套權(quán)限控制最核心的就是SpringMVC攔截器加Session機(jī)制。4.1 HandlerInterceptor攔截器的實(shí)現(xiàn)邏輯SpringMVC里權(quán)限校驗(yàn)最簡(jiǎn)潔的方式就是定義一個(gè)HandlerInterceptor類(lèi)重寫(xiě)preHandle方法在請(qǐng)求進(jìn)入Controller之前判斷Session里有沒(méi)有登錄標(biāo)記。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { // 未登錄重定向回登錄頁(yè) response.sendRedirect(request.getContextPath() /login.jsp); return false; } return true; } }然后在spring-mvc.xml里注冊(cè)這個(gè)攔截器并配置攔截路徑。這里有個(gè)細(xì)節(jié)必須注意攔截路徑不能寫(xiě)死成/否則登錄頁(yè)和靜態(tài)資源全被攔截就死循環(huán)了。推薦配置方式mvc:interceptors mvc:interceptor mvc:mapping path/user/**/ mvc:mapping path/admin/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ mvc:exclude-mapping path/static/**/ /mvc:interceptor /mvc:interceptors4.2 后臺(tái)管理員的角色區(qū)分與請(qǐng)求分流既然有“前臺(tái)后臺(tái)”那登錄用戶分成兩類(lèi)普通用戶和管理員。最簡(jiǎn)的做法是用戶表里加一個(gè)role字段0表示普通用戶1表示管理員。登錄時(shí)把整個(gè)用戶對(duì)象放進(jìn)Session后續(xù)每個(gè)需要權(quán)限的接口都判斷Session里的用戶對(duì)象是否存在以及role是否為1。我習(xí)慣再加一個(gè)AdminInterceptor專(zhuān)門(mén)管/admin/**路徑邏輯和LoginInterceptor類(lèi)似但多一層role判斷Object loginUser request.getSession().getAttribute(loginUser); if (loginUser instanceof User) { User user (User) loginUser; if (user.getRole() 1) { return true; } } response.sendRedirect(request.getContextPath() /login.jsp); return false;之所以單獨(dú)再寫(xiě)一個(gè)攔截器而不是在LoginInterceptor里拿到所有業(yè)務(wù)邏輯是為了兩個(gè)角色的擴(kuò)展性。以后如果還要加一個(gè)“圖書(shū)管理員”角色前臺(tái)也能訪問(wèn)某些后臺(tái)接口改起來(lái)就方便。4.3 密碼不要明文存MD5加鹽的正確做法數(shù)據(jù)庫(kù)里直接存密碼明文是答辯時(shí)最容易被打低分的點(diǎn)。其實(shí)用Spring自帶的DigestUtils就能輕松做MD5加密但單純MD5還是會(huì)被彩虹表爆破常規(guī)做法是“鹽值 MD5”。我的實(shí)現(xiàn)思路是注冊(cè)時(shí)取一段固定的鹽值比如“l(fā)ibrary_salt”和用戶輸入的密碼拼接再一起做MD5把加密結(jié)果存數(shù)據(jù)庫(kù)登錄時(shí)對(duì)用戶輸入的密碼做同樣拼接加密再和數(shù)據(jù)庫(kù)里的密文比對(duì)。String salt library_salt; String encryptedPwd DigestUtils.md5DigestAsHex((salt rawPassword).getBytes(UTF-8));這樣做的好處是哪怕兩個(gè)用戶的密碼相同只要鹽值參與拼接數(shù)據(jù)庫(kù)中存的密文也都一樣不影響驗(yàn)證邏輯但比純MD5安全了一步。如果是更規(guī)范的項(xiàng)目建議給每個(gè)用戶隨機(jī)一個(gè)鹽值然后單獨(dú)存在用戶表里不過(guò)課設(shè)和外包項(xiàng)目用固定鹽值也夠用了。4.4 登錄信息與退出登錄成功后把用戶對(duì)象放進(jìn)Session頁(yè)面上的顯示用JSTL的${sessionScope.loginUser.username}即可。退出功能就是執(zhí)行session.invalidate()把Session整個(gè)失效同時(shí)把購(gòu)物車(chē)、借閱臨時(shí)狀態(tài)一并清掉。這里提醒一句JSP頁(yè)面里取Session的值一定要加${empty sessionScope.loginUser}判斷否則未登錄用戶訪問(wèn)首頁(yè)時(shí)明明沒(méi)登錄卻能在導(dǎo)航欄看到一個(gè)用戶名非常出戲。5. 核心業(yè)務(wù)實(shí)現(xiàn)檢索分頁(yè)、借閱事務(wù)、后臺(tái)CRUD的代碼思路權(quán)限分開(kāi)后剩下的就是把各個(gè)業(yè)務(wù)功能逐個(gè)填進(jìn)SSM的鏈路。以下三個(gè)模塊基本覆蓋了這個(gè)系統(tǒng)的全部技術(shù)難點(diǎn)也是答辯時(shí)老師最想看的部分。5.1 前臺(tái)圖書(shū)檢索模糊查詢 分頁(yè)的實(shí)現(xiàn)圖書(shū)列表頁(yè)是用戶進(jìn)入系統(tǒng)后看到的第一個(gè)頁(yè)面處理不好就會(huì)又慢又亂。我在項(xiàng)目里用MyBatis的動(dòng)態(tài)SQL做一個(gè)多條件查詢支持按分類(lèi)、書(shū)名模糊搜索、作者模糊搜索。核心Mapper寫(xiě)法如下select idsearchBooks resultTypecom.example.entity.Book SELECT * FROM book where if testkeyword ! null and keyword ! AND (book_name LIKE CONCAT(%, #{keyword}, %) OR author LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND category_id #{categoryId} /if /where ORDER BY book_id DESC LIMIT #{offset}, #{pageSize} /select分頁(yè)有兩種選擇自己手寫(xiě)一個(gè)簡(jiǎn)單的PageBean或者引入PageHelper插件。如果你的項(xiàng)目只是課設(shè)我建議自己寫(xiě)邏輯非常清晰——接收當(dāng)前頁(yè)碼pageNum和每頁(yè)條數(shù)pageSize計(jì)算offset查完數(shù)據(jù)后把總記錄數(shù)拿出來(lái)算總頁(yè)數(shù)封裝成一個(gè)PageBean返回給JSP。5.2 借閱業(yè)務(wù)事務(wù)、庫(kù)存校驗(yàn)和狀態(tài)判斷借閱是Service層最典型的業(yè)務(wù)方法也是考察事務(wù)管理是否掌握的關(guān)鍵點(diǎn)。一套完整的借閱邏輯包括四步根據(jù)bookId查出圖書(shū)判斷庫(kù)存是否大于0。根據(jù)當(dāng)前登錄用戶的ID查其是否有未歸還的借閱記錄避免同一人重復(fù)借同一本書(shū)。執(zhí)行更新圖書(shū)庫(kù)存減1插入一條借閱記錄狀態(tài)為借閱中應(yīng)還時(shí)間為當(dāng)前時(shí)間加30天。上述任何一步失敗回滾所有操作。少一個(gè)判斷借閱流程就是殘缺的。尤其要注意“減庫(kù)存”和“插入借閱記錄”這兩個(gè)數(shù)據(jù)庫(kù)操作必須放進(jìn)同一個(gè)事務(wù)里。如果只減了庫(kù)存而沒(méi)插入借閱記錄圖書(shū)就莫名其妙少了一本如果只插了記錄而沒(méi)減庫(kù)存庫(kù)存數(shù)據(jù)就會(huì)虛高。所以Service方法上必須加Transactional注解Transactional(rollbackFor Exception.class) public void borrowBook(Integer userId, Integer bookId) { Book book bookMapper.selectByPrimaryKey(bookId); if (book.getStock() 0) { throw new RuntimeException(庫(kù)存不足); } int count borrowMapper.countNotReturnByUserAndBook(userId, bookId); if (count 0) { throw new RuntimeException(請(qǐng)先歸還之前借閱的圖書(shū)); } bookMapper.decreaseStock(bookId); BorrowRecord record new BorrowRecord(); // 設(shè)置userName、bookName等冗余字段 record.setUserId(userId); record.setBookId(bookId); record.setBorrowTime(new Date()); record.setReturnTime(DateUtils.addDays(new Date(), 30)); record.setStatus(0); borrowMapper.insert(record); }5.3 后臺(tái)CRUDMyBatis動(dòng)態(tài)SQL與批量操作后臺(tái)圖書(shū)管理的增刪改查核心用法是MyBatis的if動(dòng)態(tài)SQL。新增圖書(shū)時(shí)字段很多不同場(chǎng)景下傳參不同動(dòng)態(tài)SQL最適合。修改圖書(shū)時(shí)只改有值的字段避免把不需要?jiǎng)拥淖侄胃采w成null。這里還有兩個(gè)容易被忽視的業(yè)務(wù)點(diǎn)刪除圖書(shū)如果這本書(shū)還有未歸還的借閱記錄直接刪除會(huì)導(dǎo)致用戶還書(shū)時(shí)找不到對(duì)應(yīng)圖書(shū)應(yīng)該在刪除前檢查借閱表中有沒(méi)有該書(shū)的未歸還記錄。有的話提示“該書(shū)存在未歸還記錄無(wú)法刪除”。修改圖書(shū)狀態(tài)不要物理刪除圖書(shū)用一個(gè)is_delete字段標(biāo)記邏輯刪除是更穩(wěn)妥的方案。這樣即使某本書(shū)被下架歷史借閱記錄也不會(huì)變成孤兒數(shù)據(jù)。5.4 JSP頁(yè)面的數(shù)據(jù)展示JSTL EL是標(biāo)準(zhǔn)答案JSP頁(yè)面里最忌諱的就是寫(xiě)大段Java腳本片段scriptlet頁(yè)面會(huì)又丑又難維護(hù)。前臺(tái)列表頁(yè)展示循環(huán)數(shù)據(jù)時(shí)用JSTL標(biāo)簽加EL表達(dá)式干凈利落c:forEach items${pageBean.list} varbook div classbook-item h3a href${pageContext.request.contextPath}/book/detail?id${book.bookId}${book.bookName}/a/h3 p作者${book.author}/p p庫(kù)存${book.stock}/p a hrefjavascript:void(0) onclickborrowBook(${book.bookId})借閱/a /div /c:forEachJSP頁(yè)面加載完成后刷新一次的問(wèn)題很多人遇到過(guò)表單提交后瀏覽器地址欄沒(méi)有變化用戶手動(dòng)刷新導(dǎo)致表單重復(fù)提交。解決思路有兩個(gè)第一是提交后Controller使用redirect:重定向到列表頁(yè)而不是forward第二是JSP里用c:redirect或者在頁(yè)面加載時(shí)調(diào)用一段JavaScript控制歷史記錄。我在項(xiàng)目中實(shí)測(cè)下來(lái)最省心的方案還是Controller里統(tǒng)一用redirect保證刷新不會(huì)重復(fù)提交。6. 我踩過(guò)的坑亂碼、日期、孤兒數(shù)據(jù)與MySQL 8的SSL報(bào)錯(cuò)代碼寫(xiě)完之后真正常在找錯(cuò)上耗幾小時(shí)的往往不是業(yè)務(wù)邏輯而是環(huán)境類(lèi)和常識(shí)類(lèi)的坑。這些坑百分之百會(huì)在你的項(xiàng)目中遇到提前排掉后面就順了。6.1 三層亂碼JSP、請(qǐng)求、數(shù)據(jù)庫(kù)各管一截SSM項(xiàng)目出現(xiàn)中文亂碼十有八九是編碼設(shè)置不完整。亂碼需要從三個(gè)層面同時(shí)排查缺一不可頁(yè)面層JSP第一行設(shè)置% page contentTypetext/html; charsetutf-8 pageEncodingutf-8 %。請(qǐng)求層配置SpringMVC的CharacterEncodingFilter強(qiáng)制UTF-8。數(shù)據(jù)庫(kù)層JDBC連接URL里加characterEncodingutf8建表語(yǔ)句加default charsetutf8。filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping順序錯(cuò)了不行漏了更不行。我曾經(jīng)整了半天發(fā)現(xiàn)數(shù)據(jù)庫(kù)表是latin1編碼JSP改一百遍也沒(méi)用。建議建表時(shí)統(tǒng)一加上ENGINEInnoDB DEFAULT CHARSETutf8一勞永逸。6.2 日期顯示問(wèn)題JSP上的借閱時(shí)間和應(yīng)還時(shí)間數(shù)據(jù)庫(kù)datetime字段傳到頁(yè)面上默認(rèn)格式是“2024-06-01 12:00:00”看著還算正常。但如果你把日期放在輸入框中做編輯或者需要顯示成“2024年6月1日”這種格式就得做格式化。JSTL的fmt:formatDate是最方便的選擇fmt:formatDate value${record.borrowTime} patternyyyy-MM-dd HH:mm:ss/后臺(tái)借閱管理里經(jīng)常要根據(jù)應(yīng)還時(shí)間判斷逾期千萬(wàn)別在Java代碼里把日期轉(zhuǎn)成字符串比較直接用new Date()和數(shù)據(jù)庫(kù)里查出來(lái)的Date對(duì)象做after、before比較前端只管顯示格式化后端只管拿對(duì)象比較這樣不會(huì)出時(shí)區(qū)偏差。6.3 邏輯刪除導(dǎo)致的孤兒數(shù)據(jù)坑前面提到過(guò)借閱表不要物理外鍵這里再提醒一個(gè)相關(guān)坑如果你用了物理外鍵刪除圖書(shū)分類(lèi)時(shí)只要該分類(lèi)下還有圖書(shū)數(shù)據(jù)庫(kù)就會(huì)拒絕刪除。很多人的做法是改成“邏輯刪除”——分類(lèi)表加一個(gè)is_delete字段刪除時(shí)執(zhí)行update而不是delete。這本身沒(méi)錯(cuò)但緊接著就會(huì)遇到新問(wèn)題統(tǒng)計(jì)某分類(lèi)下圖書(shū)數(shù)量時(shí)、前臺(tái)按分類(lèi)展示時(shí)都要記得在SQL里加WHERE is_delete 0否則下架的舊分類(lèi)和舊圖書(shū)全會(huì)出現(xiàn)在頁(yè)面上。我在最終演示前就栽過(guò)一次明明做了邏輯刪除前臺(tái)分類(lèi)導(dǎo)航里還顯示著已刪除的分類(lèi)就是因?yàn)椴樵儠r(shí)漏了is_delete條件。6.4 IDEA部署刷新慢、JSP修改不生效的問(wèn)題操作系統(tǒng)SSM項(xiàng)目時(shí)還會(huì)遇到一個(gè)現(xiàn)象改了JSP重啟Tomcat后看到的還是舊頁(yè)面。這跟瀏覽器緩存和Tomcat的JSP編譯緩存都有關(guān)系。排查時(shí)先按CtrlF5強(qiáng)制刷新瀏覽器還不行就手動(dòng)清理Tomcat的work目錄下該項(xiàng)目的編譯緩存。IDEA里也可以把Tomcat的On frame deactivation設(shè)為Update classes and resources改JSP能熱更新但改Java代碼還是需要重啟別指望完全熱部署。7. 項(xiàng)目跑通之后從IDEA到服務(wù)器完成一次可演示的部署交付很多人在IDEA里跑通了項(xiàng)目就以為戰(zhàn)斗結(jié)束了其實(shí)考試和交付場(chǎng)景下最常見(jiàn)的要求是“在服務(wù)器上能訪問(wèn)”。這個(gè)環(huán)節(jié)雖然不需要你精通運(yùn)維但幾個(gè)核心步驟必須有概念。7.1 打war包還是直接打包SSM項(xiàng)目的JSP頁(yè)面在webapp目錄下通常用IDEA的Artifacts打war包或者M(jìn)aven配置packagingwar/packaging后執(zhí)行package。war包的好處是Tomcat直接扔進(jìn)webapps目錄就能跑無(wú)需額外配置。如果你的項(xiàng)目用Maven構(gòu)建打包前注意資源文件的問(wèn)題mapper.xml如果放在src/main/java的包下可能不會(huì)被打進(jìn)war包需要在pom.xml的build節(jié)點(diǎn)里加上resources配置把mapper文件也包含進(jìn)去。這個(gè)問(wèn)題相當(dāng)常見(jiàn)我見(jiàn)過(guò)好幾次“打包后在服務(wù)器上報(bào)MapperBindingException本地卻好好的”八成就是mapper.xml沒(méi)打進(jìn)去。7.2 云服務(wù)器上部署的完整流程在服務(wù)器上的部署流程按這個(gè)順序操作基本不會(huì)翻車(chē)服務(wù)器安裝JDK、Tomcat、MySQL版本和本地保持一致。把本地的library數(shù)據(jù)庫(kù)導(dǎo)出為SQL文件在服務(wù)器MySQL里執(zhí)行導(dǎo)入。修改數(shù)據(jù)庫(kù)配置文件把localhost換成服務(wù)器內(nèi)網(wǎng)地址或公網(wǎng)地址賬號(hào)密碼改成生產(chǎn)環(huán)境的強(qiáng)密碼。把war包上傳到Tomcat的webapps目錄啟動(dòng)Tomcat。查看Tomcat日志catalina.out確認(rèn)啟動(dòng)成功再通過(guò)瀏覽器訪問(wèn)http://服務(wù)器IP:8080/項(xiàng)目名/。如果訪問(wèn)時(shí)報(bào)404先檢查T(mén)omcat啟動(dòng)日志里有沒(méi)有“Deployment of web application archive ... has finished”字樣如果報(bào)數(shù)據(jù)庫(kù)連接錯(cuò)誤優(yōu)先查防火墻是否放通了3306端口以及MySQL是否允許遠(yuǎn)程連接user表里host是否是%。這些點(diǎn)排查完部署問(wèn)題就解決了大半。7.3 交付時(shí)怎么展示這個(gè)項(xiàng)目最加分最后說(shuō)點(diǎn)實(shí)在的交付或者答辯時(shí)代碼量不是重點(diǎn)功能鏈路完整才是重點(diǎn)。站位從“我寫(xiě)了多少個(gè)頁(yè)面”轉(zhuǎn)變成“我把一條數(shù)據(jù)從用戶點(diǎn)擊到數(shù)據(jù)庫(kù)落庫(kù)走通了”。演示時(shí)建議按這條主線走前臺(tái)注冊(cè)用戶 → 登錄 → 搜索一本書(shū) → 借閱成功 → 庫(kù)存減一 → 切到管理員賬號(hào) → 后臺(tái)看到這條借閱記錄 → 歸還該書(shū) → 前臺(tái)用戶看到已歸還狀態(tài)。這一條鏈路里任何一個(gè)環(huán)節(jié)能講出數(shù)據(jù)流轉(zhuǎn)的原理比如庫(kù)存是在哪個(gè)事務(wù)里減的、Session里存了什么、攔截器攔了誰(shuí)比你在臺(tái)上背一百句“基于SSM實(shí)現(xiàn)了什么功能”都管用。再補(bǔ)充一個(gè)展示技巧把數(shù)據(jù)庫(kù)客戶端分屏放在旁邊。當(dāng)你在前臺(tái)頁(yè)面點(diǎn)下“借閱”按鈕時(shí)現(xiàn)場(chǎng)指著數(shù)據(jù)表里新增的那條記錄說(shuō)“看庫(kù)里的stock從3變成2了”這種直觀的演示比任何PPT截圖都有說(shuō)服力。我在實(shí)際帶這個(gè)項(xiàng)目時(shí)最深的體會(huì)是圖書(shū)管理系統(tǒng)這種老生常談的題目難度從來(lái)不在于它看起來(lái)有多簡(jiǎn)單而在于你能不能把那些“大家都這么寫(xiě)但都不說(shuō)為什么”的細(xì)節(jié)處理好——為什么要邏輯刪除、為什么借閱表要冗余字段、為什么減庫(kù)存和插記錄必須放進(jìn)同一個(gè)事務(wù)。這些細(xì)節(jié)才是讓老師相信你真的掌握了SSM和MySQL而不是背了份代碼的關(guān)鍵。最后分享一個(gè)我在收尾階段經(jīng)常會(huì)做的小檢查把項(xiàng)目從IDEA里原封不動(dòng)部署到一臺(tái)干凈服務(wù)器上如果中間不依賴任何本地的緩存和配置就能跑通那這個(gè)項(xiàng)目才算真正交付完成。