生家教課程訂購(gòu)平臺(tái)實(shí)戰(zhàn):權(quán)限控制與訂單狀態(tài)機(jī)設(shè)計(jì))
1. 一個(gè)家教課程訂購(gòu)項(xiàng)目背后真正的難點(diǎn)角色權(quán)限與訂單狀態(tài)流轉(zhuǎn)1.1 為什么選擇SSM而不是Spring Boot先說結(jié)論如果你是為了快速交差或者生產(chǎn)環(huán)境落地我肯定會(huì)推薦Spring Boot但如果你拿到的題目明確是java_ssm52大學(xué)生家教課程訂購(gòu)平臺(tái)那大概率是教學(xué)體系或者畢業(yè)設(shè)計(jì)框架里對(duì)SSM有硬性要求。這個(gè)項(xiàng)目源碼我在IDEA里完整跑通過也帶著幾個(gè)學(xué)生從零搭建過SSM這套東西放在今天看確實(shí)有點(diǎn)老派但它對(duì)理解Java Web底層的幫助是Spring Boot給不了的。SSM指的是Spring Spring MVC MyBatis三件套。Spring負(fù)責(zé)控制反轉(zhuǎn)和面向切面編程Spring MVC負(fù)責(zé)請(qǐng)求路由和參數(shù)綁定MyBatis負(fù)責(zé)SQL與Java對(duì)象的映射。這三個(gè)框架各管一段鏈路非常清晰瀏覽器發(fā)請(qǐng)求到DispatcherServletHandlerMapping找到對(duì)應(yīng)ControllerController調(diào)用Service層Service層通過Mapper接口操作數(shù)據(jù)庫(kù)最后ModelAndView或者JSON返回給前端。每一條請(qǐng)求你都看得見摸得著不像Spring Boot那樣把很多東西自動(dòng)配置掉了出了問題反而不好定位。對(duì)于學(xué)習(xí)階段的人來說SSM的麻煩恰恰是它的教學(xué)價(jià)值。1.2 項(xiàng)目的三層角色模型拆解這個(gè)家教課程訂購(gòu)平臺(tái)涉及三種核心角色大學(xué)生學(xué)生端、教師授課端、管理員平臺(tái)端。我見過很多人做這種多角色項(xiàng)目一上來就寫一堆if判斷最后代碼亂成一鍋粥。正確的做法是先想清楚每種角色能干什么、不能干什么再把權(quán)限規(guī)則落到Spring MVC的攔截器里去。學(xué)生端核心訴求是瀏覽課程、按科目或者價(jià)格篩選、下單購(gòu)買、查看已購(gòu)課程、對(duì)教師進(jìn)行評(píng)價(jià)。教師端核心訴求是申請(qǐng)成為教師、發(fā)布課程、查看自己被訂購(gòu)的情況、管理課程上下架。管理員端核心訴求是審核教師入駐申請(qǐng)、審核課程內(nèi)容、處理訂單異常、統(tǒng)計(jì)平臺(tái)數(shù)據(jù)。這三種角色的菜單和操作邊界完全不同我建議在菜單層面就用session里的role字段做區(qū)分而不是讓前端通過路由隱藏頁(yè)面。原因很簡(jiǎn)單前端隱藏只是UI層面的后端如果不做攔截懂點(diǎn)技術(shù)的人直接拼接URL就能訪問管理接口這是真實(shí)項(xiàng)目中特別常見的漏洞。SSM里用HandlerInterceptor統(tǒng)一處理非常合適。2. IDEA中搭建SSM聚合工程的具體步驟2.1 工程結(jié)構(gòu)與Maven依賴配置我在IDEA里推薦用Maven的多模塊結(jié)構(gòu)雖然單模塊也能跑但多模塊更貼近真實(shí)團(tuán)隊(duì)協(xié)作習(xí)慣。建議拆成三個(gè)模塊ssm-common通用工具類和常量、ssm-dao數(shù)據(jù)訪問層Mapper接口和XML、ssm-webController層、Service層、前端頁(yè)面和配置文件。Maven依賴這塊有幾個(gè)關(guān)鍵點(diǎn)Spring核心依賴、Spring MVC依賴、MyBatis和MyBatis-Spring橋接包、數(shù)據(jù)庫(kù)驅(qū)動(dòng)、連接池commonly用的是Druid、Jackson處理JSON、JSP標(biāo)準(zhǔn)標(biāo)簽庫(kù)JSTL。版本選擇上我踩過一次坑Spring 4.x和Spring 5.x對(duì)javax.servlet-api的依賴版本要求不一樣如果你用的是Tomcat 8.5以上建議直接上Spring 5.2.x否則會(huì)出現(xiàn)ClassNotFound異常排查起來很折磨人。一個(gè)可以直接參考的pom.xml核心片段dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.2.15.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.2.15.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.6/version /dependency /dependencies2.2 配置文件里最容易踩的坑數(shù)據(jù)庫(kù)連接與事務(wù)SSM的配置文件比Spring Boot多常見的有spring-mvc.xml、spring-mybatis.xml、web.xml、jdbc.properties、mybatis-config.xml。大多數(shù)初學(xué)者都會(huì)在這一步卡住原因往往是配置文件名稱不匹配或者掃描包路徑寫錯(cuò)。我給大家一個(gè)排查思路先拆開驗(yàn)證不要一次性合起來跑。先啟動(dòng)一個(gè)只帶spring-context的最小配置確保Bean能創(chuàng)建再引入MyBatis確保Mapper接口能被掃描到且能創(chuàng)建代理對(duì)象最后加入Spring MVC的DispatcherServlet配置。每一步單獨(dú)驗(yàn)證出了問題就知道是哪一層的。jdbc.properties部分注意數(shù)據(jù)庫(kù)連接串編碼要和數(shù)據(jù)庫(kù)一致jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/tutor_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.passwordyourpassword事務(wù)配置極其重要尤其是訂單這種涉及多表寫入的操作不配事務(wù)就是給自己埋雷。在spring-mybatis.xml里用DataSourceTransactionManager加上tx:advice聲明service層所有方法都受事務(wù)控制默認(rèn)timeout不要設(shè)太短下單流程要校驗(yàn)庫(kù)存、寫訂單表、寫訂單詳情表三步下來很容易超過3秒默認(rèn)值。我的建議是設(shè)為15秒并且把只讀查詢單獨(dú)放一個(gè)事務(wù)屬性。3. 數(shù)據(jù)庫(kù)設(shè)計(jì)圍繞課程訂購(gòu)這條主線的核心表3.1 用戶體系表設(shè)計(jì)很多學(xué)生項(xiàng)目會(huì)把學(xué)生和教師放在同一張user表里用type字段區(qū)分。這個(gè)選擇本身沒錯(cuò)但要注意教師比學(xué)生多了很多屬性比如教學(xué)科目、教學(xué)年限、個(gè)人簡(jiǎn)介、審核狀態(tài)。如果全塞在user表里字段會(huì)很雜。我用的是基礎(chǔ)用戶表 角色擴(kuò)展表模式user表只保存登錄憑證和公共資料用戶名、密碼、手機(jī)號(hào)、頭像、注冊(cè)時(shí)間、角色類型teacher_info表單獨(dú)存教師的擴(kuò)展信息兩張表通過user_id關(guān)聯(lián)。密碼存儲(chǔ)這塊我多說一句。雖然這是一個(gè)課程設(shè)計(jì)級(jí)別的項(xiàng)目但用MD5直接存明文密碼的做法真的別再用了網(wǎng)上隨便一個(gè)在線MD5庫(kù)都能撞出來。至少用Spring的BCryptPasswordEncoder加鹽加密。這個(gè)類在spring-security-crypto包里單獨(dú)引入這個(gè)依賴就能用不需要拉入整個(gè)Spring Security框架很輕量。3.2 課程與訂單表的約束設(shè)計(jì)課程表course的核心字段包括課程名稱、所屬科目分類、教師ID外鍵關(guān)聯(lián)teacher_info、課程簡(jiǎn)介、課時(shí)數(shù)、單價(jià)、封面圖、上架狀態(tài)、創(chuàng)建時(shí)間。這里有個(gè)細(xì)節(jié)很多人沒注意到課程表的價(jià)格字段最好用decimal(10,2)不要用float或者double。因?yàn)樯婕坝唵谓痤~計(jì)算二進(jìn)制浮點(diǎn)數(shù)在累加時(shí)會(huì)出現(xiàn)精度丟失比如0.1加0.2在Java里用double計(jì)算結(jié)果是0.30000000000000004這在金額場(chǎng)景是絕對(duì)不允許的。訂單主表orders和訂單詳情表order_item是按業(yè)務(wù)需要設(shè)計(jì)的。一個(gè)訂單可以包含多個(gè)課程所以主表管訂單整體狀態(tài)、總金額、下單用戶ID、下單時(shí)間、支付狀態(tài)、支付時(shí)間詳情表管具體購(gòu)買了哪門課程、單價(jià)、購(gòu)買數(shù)量、對(duì)應(yīng)教師ID。外鍵關(guān)系是orders一對(duì)多order_item。訂單表盡量不做物理刪除只用狀態(tài)字段標(biāo)記取消或完成這樣后續(xù)統(tǒng)計(jì)報(bào)表才有真實(shí)數(shù)據(jù)可用。3.3 訂單狀態(tài)機(jī)的實(shí)現(xiàn)思路訂單狀態(tài)是這個(gè)項(xiàng)目最核心的業(yè)務(wù)邏輯。我設(shè)計(jì)的狀態(tài)字段是一個(gè)int值從0到40表示待支付1表示支付成功2表示已取消3表示已完成4表示申請(qǐng)退款。狀態(tài)變化遵循嚴(yán)格的方向約束待支付可以走支付成功也可以走取消支付成功可以走申請(qǐng)退款也可以走課程完結(jié)已完成申請(qǐng)退款只能走到退款成功或者拒絕退款。如果你不想讓狀態(tài)像野馬一樣亂跳就在Service層做一個(gè)狀態(tài)流轉(zhuǎn)校驗(yàn)方法每次更新訂單之前先判斷當(dāng)前狀態(tài)是否允許跳轉(zhuǎn)到目標(biāo)狀態(tài)。我試過在項(xiàng)目里加一張order_status_log表專門記錄每一次狀態(tài)變更的操作人、操作時(shí)間和備注。這個(gè)表看起來不起眼但對(duì)調(diào)試幫助非常大尤其是訂單狀態(tài)和用戶反饋對(duì)不上時(shí)查這張表就能知道到底哪一步出了問題。這個(gè)設(shè)計(jì)在答辯環(huán)節(jié)也是加分項(xiàng)。4. 核心功能實(shí)現(xiàn)課程檢索、下單、支付模擬4.1 課程分頁(yè)搜索PageHelper還是手寫SQL課程列表頁(yè)的核心是分頁(yè)搜索條件通常包含科目分類、價(jià)格區(qū)間、教師姓名關(guān)鍵詞、上架狀態(tài)。如果只對(duì)管理員顯示課程管理列表還要加上審核狀態(tài)。這里我推薦用PageHelper插件配合Mapper XML手寫SQL。雖然MyBatis-Plus等增強(qiáng)框架有更好的內(nèi)置分頁(yè)能力但在SSM項(xiàng)目里PageHelper已經(jīng)是社區(qū)驗(yàn)證過的成熟方案。用法很簡(jiǎn)單在Service層查詢前調(diào)用PageHelper.startPage(pageNum, pageSize)后面緊跟的第一次Mapper查詢就會(huì)自動(dòng)拼上LIMIT。返回的結(jié)果用PageInfo包裝這樣前端就能拿到總記錄數(shù)、總頁(yè)數(shù)、當(dāng)前頁(yè)數(shù)據(jù)這些完整分頁(yè)信息。需要提醒的是PageHelper的startPage只作用于緊接著的一次查詢?nèi)绻阍谡{(diào)用startPage之后做了兩次Mapper查詢第二次查詢也會(huì)被意外分頁(yè)這是一個(gè)非常經(jīng)典的誤用場(chǎng)景。我的建議是startPage和查詢方法一定要貼在一起寫中間不要插入任何其他Mapper調(diào)用。4.2 下單流程的并發(fā)控制與防重提學(xué)生點(diǎn)擊購(gòu)買按鈕后端下單流程是這樣的校驗(yàn)用戶是否登錄、校驗(yàn)課程是否上架、校驗(yàn)課程庫(kù)存、創(chuàng)建訂單主表記錄、創(chuàng)建訂單詳情記錄、扣減課程剩余名額、跳轉(zhuǎn)支付。并發(fā)問題出現(xiàn)在校驗(yàn)庫(kù)存到扣減庫(kù)存這個(gè)時(shí)間窗口。兩個(gè)用戶同時(shí)下單同一門只剩1個(gè)名額的課程都通過了校驗(yàn)然后各自扣減最后庫(kù)存變成-1。解決這種問題常規(guī)方案有樂觀鎖和悲觀鎖在SSM項(xiàng)目里我用的是比較務(wù)實(shí)的做法在課程表增加一個(gè)version字段扣庫(kù)存的SQL里帶上version條件影響行數(shù)為0就說明版本沖突讓用戶重新下單或者提示庫(kù)存不足。實(shí)際執(zhí)行的更新語(yǔ)句大概是這樣的UPDATE course SET stock stock - 1, version version 1 WHERE id #{courseId} AND stock 0 AND version #{version}這句SQL本身就把庫(kù)存大于0的判斷放在條件里效果等同于樂觀鎖即使沒有version字段stock 0這個(gè)條件也能兜住超賣問題。防重提則靠一個(gè)簡(jiǎn)單的下單方法冪等控制在訂單主表加user_id和create_time的聯(lián)合索引下單前先查一下最近30秒內(nèi)是否已有同用戶待支付訂單如果有直接返回請(qǐng)先處理已有訂單。4.3 模擬支付與訂單狀態(tài)回調(diào)真實(shí)項(xiàng)目接入微信或支付寶支付需要商戶號(hào)、證書、回調(diào)域名一堆東西學(xué)生項(xiàng)目通常不需要也不可能有這些條件。這個(gè)項(xiàng)目我建議做兩個(gè)入口一個(gè)是模擬支付跳轉(zhuǎn)頁(yè)展示訂單金額和支付二維碼樣式的占位圖點(diǎn)擊模擬支付成功按鈕直接調(diào)支付成功回調(diào)接口另一個(gè)是訂單列表里的模擬支付失敗按鈕用來測(cè)試支付失敗后訂單保持待支付狀態(tài)的處理邏輯。回調(diào)接口的核心動(dòng)作是更新訂單狀態(tài)為已支付并且記錄支付時(shí)間。這里有個(gè)很容易被忽視的點(diǎn)回調(diào)接口要做冪等校驗(yàn)。因?yàn)橛脩艨赡芤驗(yàn)榫W(wǎng)絡(luò)原因多次點(diǎn)擊模擬支付成功如果回調(diào)接口不做判斷直接覆蓋狀態(tài)就會(huì)把已取消的訂單誤標(biāo)成已支付。我的做法是先根據(jù)訂單號(hào)查訂單當(dāng)前狀態(tài)只有待支付狀態(tài)的訂單才允許改為已支付否則直接返回操作失敗。5. 管理端與統(tǒng)計(jì)功能怎么做得不雞肋5.1 教師審核與課程上下架管理端最核心的兩個(gè)流程是教師入駐審核和課程上下架管理。教師注冊(cè)后狀態(tài)默認(rèn)是待審核只有管理員審核通過后教師才能登錄系統(tǒng)發(fā)布課程。審核流程的業(yè)務(wù)邏輯其實(shí)就是一個(gè)狀態(tài)更新操作但真正的難點(diǎn)在于狀態(tài)改了以后相關(guān)頁(yè)面要能實(shí)時(shí)感知。比如教師登錄后被提示審核中暫無法發(fā)布課程他無法立刻看到自己已經(jīng)通過審核因?yàn)榈卿洉r(shí)把角色狀態(tài)寫死在session里了。我用了一個(gè)變通方案不再把審核狀態(tài)常駐session而是每次教師進(jìn)入發(fā)布課程頁(yè)面時(shí)實(shí)時(shí)從數(shù)據(jù)庫(kù)查一份最新狀態(tài)這樣管理員那邊審核通過教師刷新頁(yè)面就能生效不用重復(fù)登錄。課程上下架管理更簡(jiǎn)單直接管理員列表里展示所有課程的當(dāng)前狀態(tài)點(diǎn)上架/下架按鈕時(shí)調(diào)一個(gè)接口切換狀態(tài)字段。這里注意課程下架時(shí)要順帶判斷有沒有未完成的訂單正在關(guān)聯(lián)該課程如果有提示管理員先處理訂單不要粗暴下架否則用戶體驗(yàn)會(huì)很差。5.2 數(shù)據(jù)統(tǒng)計(jì)的簡(jiǎn)單實(shí)現(xiàn)近7日訂單量每個(gè)管理后臺(tái)都要有一個(gè)數(shù)據(jù)看板學(xué)生項(xiàng)目一般不需要做復(fù)雜的圖表但至少要有幾個(gè)核心數(shù)字注冊(cè)用戶總數(shù)、付費(fèi)訂單總數(shù)、平臺(tái)總流水、近7日訂單趨勢(shì)。近7日訂單量的SQL寫法有一定的代表性我直接給出思路以日期為分組維度統(tǒng)計(jì)每天已支付訂單的數(shù)量和金額。有人會(huì)用GROUP BY DATE_FORMAT(create_time, %Y-%m-%d)這沒問題但會(huì)有一個(gè)坑某天沒有訂單時(shí)這一天的數(shù)據(jù)就是空的圖表上會(huì)斷檔。更專業(yè)的做法是先在Java代碼里生成一個(gè)近7天的日期列表然后逐天查訂單量沒有訂單的日期補(bǔ)0。SELECT DATE(create_time) AS order_date, COUNT(*) AS order_count, SUM(total_amount) AS total_amount FROM orders WHERE status 1 GROUP BY DATE(create_time) ORDER BY order_date DESC6. 從IDEA啟動(dòng)到部署Tomcat配置與常見問題排查6.1 本地運(yùn)行環(huán)境準(zhǔn)備我強(qiáng)烈建議統(tǒng)一環(huán)境版本不然真的會(huì)很折磨人。JDK用1.8Maven用3.6.3Tomcat用8.5.xMySQL用5.7或者8.0都可以。IDEA版本不太挑2019及以上版本都能正常跑SSM項(xiàng)目。導(dǎo)入項(xiàng)目到IDEA時(shí)注意不要直接Open整個(gè)文件夾建議用Maven方式導(dǎo)入File - Open選擇pom.xmlIDEA會(huì)識(shí)別出這是一個(gè)Maven項(xiàng)目并自動(dòng)下載依賴。如果網(wǎng)絡(luò)狀況不好第一次構(gòu)建會(huì)卡很長(zhǎng)時(shí)間這也是很多學(xué)生一上來就放棄的坎。解決辦法是給Maven配置阿里云鏡像。Tomcat配置方面IDEA里點(diǎn)Run - Edit Configurations添加Tomcat Server - Local設(shè)置好Tomcat的安裝目錄后在Deployment頁(yè)簽里添加Artifact選擇war exploded。用war exploded模式的好處是支持熱部署改完JSP或者Java類后不用頻繁重啟。訪問路徑建議設(shè)置成/否則每次打開都是http://localhost:8080/項(xiàng)目名/頁(yè)面里干的相對(duì)路徑還容易出問題。6.2 我實(shí)測(cè)遇到過的三個(gè)典型運(yùn)行問題第一個(gè)典型問題是數(shù)據(jù)庫(kù)連接串沒有加serverTimezone參數(shù)MySQL 8.0以上會(huì)直接報(bào)CLIENT_PLUGIN_AUTH或者Communications link failure。解決辦法是在jdbc.url后面加上serverTimezoneAsia/Shanghai記得把驅(qū)動(dòng)換成com.mysql.cj.jdbc.Driver。第二個(gè)典型問題是訪問首頁(yè)出現(xiàn)了Tomcat默認(rèn)的404錯(cuò)誤頁(yè)面排查半天發(fā)現(xiàn)是因?yàn)閣eb.xml里的DispatcherServlet攔截了/請(qǐng)求但沒有配置HomeController的RequestMapping為/index。這個(gè)問題的本質(zhì)是Controller映射和服務(wù)端配置對(duì)不上建議把首頁(yè)請(qǐng)求映射到Controller里一個(gè)顯示課程列表的方法而不是直接找靜態(tài)index.jsp。因?yàn)槭醉?yè)需要從數(shù)據(jù)庫(kù)加載課程數(shù)據(jù)走Controller才合理。第三個(gè)典型問題是JSP頁(yè)面無法顯示JSTL標(biāo)簽頁(yè)面上直接輸出大段代碼。原因是pom.xml里引入JSTL的坐標(biāo)不對(duì)或者Tomcat運(yùn)行時(shí)沒有把jstl-api和jstl-impl都帶上。更常見的坑是引入的方式混淆了Java EE的舊包和Jakarta的新包。SSM項(xiàng)目里用javax.servlet:jstl版本1.2就行別引成2.x的新坐標(biāo)。6.3 項(xiàng)目源碼如何使用與二次開發(fā)方向如果你拿到的是完整源碼第一步不要急著跑先看README或者項(xiàng)目結(jié)構(gòu)說明。把數(shù)據(jù)庫(kù)腳本導(dǎo)入MySQL檢查jdbc.properties里的賬號(hào)密碼是否和自己本地一致。然后用IDEA以Maven項(xiàng)目方式打開等待依賴下載完成后配置好Tomcat啟動(dòng)即可。這個(gè)項(xiàng)目可以擴(kuò)展的方向其實(shí)很多我梳理了幾個(gè)比較接地氣的方向第一是引入Redis做課程熱榜緩存減少高頻課程列表對(duì)數(shù)據(jù)庫(kù)的壓力第二是在線支付接入真實(shí)沙箱環(huán)境把模擬支付替換成支付寶的沙箱API這個(gè)在簡(jiǎn)歷上是實(shí)打?qū)嵉牧咙c(diǎn)第三是增加站內(nèi)信功能教師收到新訂單提醒、管理員收到新入駐申請(qǐng)?zhí)嵝训谒氖钦n程評(píng)價(jià)體系細(xì)化為多維度評(píng)分比如講解清晰度、課程難度、時(shí)間守時(shí)性。我個(gè)人的建議是如果要用這個(gè)項(xiàng)目參加面試一定要能講清楚兩件事一是訂單并發(fā)控制是怎么做的二是權(quán)限攔截是怎么實(shí)現(xiàn)的。這兩塊是面試官最愛深入追問的點(diǎn)你理解越透徹項(xiàng)目就越有說服力。如果只是做課程設(shè)計(jì)作業(yè)把狀態(tài)機(jī)和統(tǒng)計(jì)功能做扎實(shí)配合一份清晰的文檔和截圖拿到一個(gè)理想的成績(jī)基本沒有問題。