全棧實(shí)戰(zhàn):數(shù)據(jù)庫(kù)設(shè)計(jì)、JWT鑒權(quán)與部署)
春招那會(huì)兒我接過(guò)不少私活其中很大一類(lèi)就是幫本科生把畢業(yè)設(shè)計(jì)從能跑打磨到能答辯。做得最多的就是Spring Boot管理系統(tǒng)什么圖書(shū)借閱、宿舍管理、校園考勤換一層業(yè)務(wù)殼子骨架都大同小異。但說(shuō)實(shí)話基于Spring Boot家教管理系統(tǒng)的設(shè)計(jì)與實(shí)現(xiàn)這類(lèi)題目是我覺(jué)得最有業(yè)務(wù)味道、也最能體現(xiàn)設(shè)計(jì)能力的一個(gè)選題。它不像圖書(shū)管理那樣只對(duì)著CRUD猛懟里面既有用戶(hù)角色劃分、又有訂單和課時(shí)結(jié)算這種完整流程還牽扯到權(quán)限控制和時(shí)間沖突校驗(yàn)。今天我就以這個(gè)題目為例子把我做這類(lèi)項(xiàng)目的完整思路、表結(jié)構(gòu)設(shè)計(jì)、后端落地、前端聯(lián)調(diào)、以及那些文檔里絕對(duì)不會(huì)寫(xiě)的坑一次性講清楚。這篇文章適合誰(shuí)適合正在做家教管理系統(tǒng)畢設(shè)的同學(xué)適合想系統(tǒng)地走一遍Spring Boot全棧項(xiàng)目的初學(xué)者也適合接單幫別人做畢設(shè)的開(kāi)發(fā)同行。我會(huì)按照真實(shí)項(xiàng)目的推進(jìn)順序來(lái)寫(xiě)從選題理由到數(shù)據(jù)庫(kù)設(shè)計(jì)從JWT鑒權(quán)到定時(shí)任務(wù)每一段都是實(shí)操后的總結(jié)不是那種只給截圖不給代碼的教程。1. 為什么我推薦用Spring Boot做家教管理系統(tǒng)選題與技術(shù)選型的真實(shí)考量1.1 家教業(yè)務(wù)的核心痛點(diǎn)信息不對(duì)稱(chēng)與流程管理先聊一個(gè)最基礎(chǔ)的問(wèn)題家教管理系統(tǒng)到底要解決什么如果你只是把它當(dāng)成一個(gè)增刪改查的練習(xí)那答辯的時(shí)候很容易被問(wèn)住。家教行業(yè)本質(zhì)上是一個(gè)撮合平臺(tái)牽涉到的角色至少有四類(lèi)家長(zhǎng)、學(xué)生、家教老師、平臺(tái)管理員。傳統(tǒng)模式下大家靠微信群和電話溝通信息極度分散上課時(shí)間靠口頭約定課時(shí)費(fèi)算不清楚老師放鴿子了家長(zhǎng)也沒(méi)處投訴。所以一個(gè)合格的家教管理系統(tǒng)至少要管住三件事誰(shuí)來(lái)教、誰(shuí)來(lái)學(xué)、怎么結(jié)算。老師和學(xué)生的匹配關(guān)系、課程的時(shí)間安排、課后的評(píng)價(jià)與課時(shí)確認(rèn)這三條線是最核心的。你去看很多高分畢設(shè)論文功能列表拉得很長(zhǎng)但業(yè)務(wù)主線也就是這三條。想明白這一點(diǎn)你在做需求分析的時(shí)候就不會(huì)跑偏。這也是為什么我不建議把題目改成家教信息發(fā)布系統(tǒng)或者家教預(yù)約平臺(tái)——管理這兩個(gè)字意味著你要做的不僅僅是一個(gè)信息墻而是一套有狀態(tài)流轉(zhuǎn)、有角色權(quán)限、有業(yè)務(wù)流程的后臺(tái)系統(tǒng)這恰恰是Spring Boot最擅長(zhǎng)的領(lǐng)域。1.2 技術(shù)選型對(duì)比Spring Boot、SSM、SSH該怎么選很多同學(xué)在開(kāi)題的時(shí)候會(huì)糾結(jié)學(xué)校老師讓用SSMSpring SpringMVC MyBatis我看網(wǎng)上全是Spring Boot的教程到底選哪個(gè)我的建議很直接優(yōu)先Spring Boot除非導(dǎo)師硬性規(guī)定SSM。理由有三個(gè)第一Spring Boot的自動(dòng)配置機(jī)制幫你省掉了大量XML配置尤其是數(shù)據(jù)源、事務(wù)管理器、攔截器這些東西以前在SSM里要手寫(xiě)一堆配置文件Spring Boot通過(guò)starter依賴(lài)和application.yml幾行配置全搞定了。這不是偷懶而是把精力騰出來(lái)放在業(yè)務(wù)邏輯上。第二招聘市場(chǎng)和開(kāi)源社區(qū)早就倒向Spring Boot了你寫(xiě)簡(jiǎn)歷的時(shí)候精通Spring Boot比熟悉SSM有說(shuō)服力得多。哪怕你只是做畢設(shè)用Spring Boot也是給自己攢經(jīng)驗(yàn)。第三Spring Boot 2.x和3.x選誰(shuí)的問(wèn)題。我的建議是選2.7.x。為什么因?yàn)?.x要求JDK 17起步而且很多第三方starter的兼容性還停留在2.x時(shí)代。你如果是為了穩(wěn)定把畢設(shè)做完JDK 8 Spring Boot 2.7.x MyBatis-Plus這套組合最穩(wěn)踩坑最少。當(dāng)然如果你的導(dǎo)師要求新那Spring Boot 3.2 JDK 17也可以但你要做好某些老教程代碼不能直接抄的心理準(zhǔn)備。2. 需求邊界畫(huà)清楚后面寫(xiě)代碼才不擰巴2.1 四類(lèi)角色的權(quán)限劃分管理系統(tǒng)的核心思想就是不同人看到不同的東西。家教系統(tǒng)里我通常設(shè)置四種角色權(quán)限從上到下是角色核心操作范圍特殊能力管理員全站用戶(hù)管理、科目管理、訂單總覽強(qiáng)制下架違規(guī)老師、處理投訴家長(zhǎng)發(fā)布需求、搜索老師、發(fā)起預(yù)約對(duì)已完成的課時(shí)進(jìn)行確認(rèn)/評(píng)價(jià)家教老師發(fā)布可授課程、查看預(yù)約、提交課時(shí)上傳資質(zhì)證明、維護(hù)可授課時(shí)間學(xué)生查看課程、提交試聽(tīng)申請(qǐng)綁定在家長(zhǎng)賬號(hào)下這里有個(gè)細(xì)節(jié)很多人不注意學(xué)生和家長(zhǎng)在業(yè)務(wù)上經(jīng)常是分離的。小學(xué)生上課是家長(zhǎng)付費(fèi)、家長(zhǎng)選老師但真正上課的是孩子。所以在數(shù)據(jù)模型里學(xué)生信息應(yīng)該獨(dú)立建表家長(zhǎng)賬號(hào)通過(guò)student_id關(guān)聯(lián)到孩子。如果你偷懶只做一個(gè)用戶(hù)表角色字段后面做家長(zhǎng)查看孩子的上課記錄這個(gè)功能時(shí)就會(huì)非常別扭。2.2 核心業(yè)務(wù)流從預(yù)約到授課到結(jié)算我建議你把業(yè)務(wù)流程先畫(huà)出來(lái)不是畫(huà)給答辯看是畫(huà)給自己寫(xiě)代碼時(shí)理清頭緒。家教系統(tǒng)的核心鏈路通常長(zhǎng)這樣家長(zhǎng)搜索老師 → 按科目、區(qū)域、價(jià)格篩選家長(zhǎng)發(fā)起預(yù)約 → 提交期望的上課時(shí)間老師確認(rèn)/拒絕 → 確認(rèn)后生成課程訂單實(shí)際上課完成 → 老師提交課時(shí)記錄家長(zhǎng)確認(rèn)課時(shí) → 系統(tǒng)自動(dòng)累計(jì)已確認(rèn)課時(shí)結(jié)算階段 → 家長(zhǎng)充值/支付老師申請(qǐng)?zhí)岈F(xiàn)這條鏈路最大的特點(diǎn)是存在狀態(tài)機(jī)。預(yù)約訂單的狀態(tài)至少要有待確認(rèn)、已確認(rèn)、已拒絕、進(jìn)行中、已完成、已取消。數(shù)據(jù)庫(kù)里最好用一個(gè)status字段專(zhuān)門(mén)存這個(gè)狀態(tài)而不是通過(guò)刪記錄來(lái)表示取消。別小看這個(gè)設(shè)計(jì)答辯老師最喜歡問(wèn)的就是訂單取消后數(shù)據(jù)怎么處理你在表里保留狀態(tài)字段直接就能答上來(lái)。2.3 容易被忽視的非功能需求很多同學(xué)做畢設(shè)只盯著功能清單結(jié)果答辯的時(shí)候被一句你這個(gè)系統(tǒng)并發(fā)怎么樣問(wèn)懵。我不主張你為了畢設(shè)去搞微服務(wù)、消息隊(duì)列這種重武器但有幾個(gè)非功能需求你要心里有數(shù)參數(shù)校驗(yàn)手機(jī)號(hào)格式、價(jià)格區(qū)間、時(shí)間格式后端的Validated注解不能省別把校驗(yàn)全丟給前端。日志記錄操作日志表要建至少記錄誰(shuí)在什么時(shí)間做了什么操作關(guān)鍵業(yè)務(wù)操作如取消訂單必須留痕。異常處理全局異常處理器要寫(xiě)不然前端動(dòng)不動(dòng)就拿到一個(gè)帶堆棧的500頁(yè)面體驗(yàn)太差。這些不是加分項(xiàng)而是基本素養(yǎng)。答辯老師可能不會(huì)逐行看代碼但如果你能主動(dòng)說(shuō)一句我用攔截器做了JWT登錄校驗(yàn)用全局異常處理器統(tǒng)一了返回格式印象分直接上一個(gè)檔次。3. 數(shù)據(jù)庫(kù)設(shè)計(jì)家教系統(tǒng)的地基怎么打3.1 核心表結(jié)構(gòu)用戶(hù)、課程、訂單、課時(shí)記錄數(shù)據(jù)庫(kù)設(shè)計(jì)這一個(gè)章節(jié)在論文里能寫(xiě)十幾頁(yè)但落到具體表結(jié)構(gòu)我建議你抓住五張主表和幾張輔助表user表用戶(hù)表CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登錄賬號(hào), password varchar(255) NOT NULL COMMENT BCrypt加密后的密碼, nickname varchar(50) DEFAULT NULL, phone varchar(20) DEFAULT NULL, role tinyint(4) NOT NULL COMMENT 1-管理員 2-家長(zhǎng) 3-老師 4-學(xué)生, status tinyint(4) DEFAULT 1 COMMENT 1-正常 0-禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;teacher_profile表老師信息擴(kuò)展表因?yàn)槔蠋熡锌颇?、教學(xué)年限、認(rèn)證狀態(tài)、課時(shí)費(fèi)等維度單獨(dú)拆一張表更清爽用user_id關(guān)聯(lián)。course_order表預(yù)約/課程訂單表CREATE TABLE course_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 訂單編號(hào), teacher_id bigint(20) NOT NULL, student_id bigint(20) NOT NULL, parent_id bigint(20) DEFAULT NULL, subject_id bigint(20) NOT NULL COMMENT 科目, start_time datetime NOT NULL COMMENT 上課開(kāi)始時(shí)間, end_time datetime NOT NULL COMMENT 上課結(jié)束時(shí)間, duration int(11) DEFAULT 2 COMMENT 課時(shí)長(zhǎng)(小時(shí)), unit_price decimal(10,2) NOT NULL COMMENT 每小時(shí)單價(jià), total_amount decimal(10,2) NOT NULL COMMENT 總價(jià), status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-待確認(rèn) 1-已確認(rèn) 2-已拒絕 3-進(jìn)行中 4-已完成 5-已取消, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;lesson_record表課時(shí)記錄表老師上完課之后提交家長(zhǎng)確認(rèn)后這個(gè)狀態(tài)才變成已完成這個(gè)表是為結(jié)算服務(wù)的。這幾張表之間的外鍵關(guān)系我不建議真的在數(shù)據(jù)庫(kù)里加物理外鍵邏輯外鍵就夠了。理由是MyBatis-Plus做聯(lián)表查詢(xún)的時(shí)候物理外鍵反而容易造成麻煩而且如果老師被刪除、但訂單還要保留歷史記錄物理外鍵會(huì)擋住這種邏輯刪除的需求。3.2 字段設(shè)計(jì)上的幾個(gè)細(xì)節(jié)決策這里分享幾個(gè)容易踩坑的點(diǎn)第一價(jià)格永遠(yuǎn)用DECIMAL不要用FLOAT或DOUBLE。浮點(diǎn)數(shù)在計(jì)算機(jī)里是近似存儲(chǔ)0.1 0.2可能等于0.30000000000000004。金額字段一旦出現(xiàn)精度問(wèn)題結(jié)算那一步就全亂了。DECIMAL(10,2)足夠應(yīng)付單價(jià)和總價(jià)。第二時(shí)間字段建議直接存datetime不要存字符串。有人圖方便把上課時(shí)間存成2025-06-01 14:00這種字符串后面如果要查某個(gè)時(shí)間段有哪些老師在教或者做時(shí)間沖突校驗(yàn)字符串比較會(huì)把你折騰瘋。MySQL的datetime字段配合BETWEEN語(yǔ)句比字符串好用一百倍。第三邏輯刪除比物理刪除安全。用戶(hù)誤操作刪除了一條課時(shí)記錄如果物理刪了數(shù)據(jù)就沒(méi)了。我一般在每張業(yè)務(wù)表加一個(gè)deleted字段0/1查詢(xún)的時(shí)候默認(rèn)過(guò)濾掉deleted1的記錄。MyBatis-Plus里用TableLogic注解就能自動(dòng)實(shí)現(xiàn)所以這個(gè)成本極低。第四訂單號(hào)要單獨(dú)生成不要用自增ID當(dāng)訂單號(hào)。自增ID暴露了業(yè)務(wù)量而且用戶(hù)看到訂單號(hào)是58這種會(huì)有種不正規(guī)感。你可以用時(shí)間戳隨機(jī)數(shù)生成訂單號(hào)比如yyyyMMddHHmmss 兩位隨機(jī)數(shù)或者直接用IdWorker.getIdStr()MyBatis-Plus自帶雪花算法實(shí)現(xiàn)一行代碼搞定。4. Spring Boot后端落地骨架搭建與關(guān)鍵接口實(shí)現(xiàn)4.1 目錄分層controller-service-mapper的職責(zé)邊界后端工程的包結(jié)構(gòu)我建議按模塊化來(lái)分而不是按層級(jí)分。什么概念按層級(jí)分是controller包下放所有Controllerservice包下放所有Service按模塊分是order包下同時(shí)有OrderController、OrderService、OrderMapper。模塊化的好處是改動(dòng)一個(gè)功能時(shí)你只需要在一個(gè)包結(jié)構(gòu)內(nèi)跳轉(zhuǎn)代碼閱讀成本低很多。推薦包結(jié)構(gòu)com.example.tutor ├── common // 通用類(lèi)返回結(jié)果、異常處理、常量 │ ├── Result.java │ ├── GlobalExceptionHandler.java │ └── BusinessException.java ├── config // 配置類(lèi)攔截器、跨域、密碼加密器 ├── security // JWT相關(guān)TokenUtil、JwtInterceptor ├── user // 用戶(hù)模塊 │ ├── UserController.java │ ├── UserService.java │ ├── UserMapper.java │ └── entity/User.java ├── order // 訂單模塊 └── lesson // 課時(shí)模塊這里一定要記住Controller只做參數(shù)接收和結(jié)果返回不寫(xiě)業(yè)務(wù)邏輯。我見(jiàn)過(guò)太多人把數(shù)據(jù)庫(kù)更新語(yǔ)句直接寫(xiě)在Controller里當(dāng)時(shí)寫(xiě)起來(lái)爽后面調(diào)試的時(shí)候想死。Service層是核心業(yè)務(wù)邏輯全部放Service里Controller保持薄薄一層。4.2 登錄認(rèn)證JWT方案在單體應(yīng)用中的實(shí)現(xiàn)家教系統(tǒng)里沒(méi)有任何一個(gè)接口可以裸奔放行除了登錄和注冊(cè)。我的做法是用JWT做無(wú)狀態(tài)認(rèn)證全套流程是用戶(hù)登錄后端校驗(yàn)用戶(hù)名密碼密碼用BCryptPasswordEncoder校驗(yàn)。校驗(yàn)通過(guò)后生成Token把用戶(hù)ID、角色封裝進(jìn)Token的claims里。前端把Token存到localStorage每次請(qǐng)求在Authorization頭帶上。后端寫(xiě)一個(gè)攔截器攔截非白名單路徑解析Token并放行。核心代碼大致是這樣Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/subject/list); } }public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(未登錄或登錄已過(guò)期); } // 解析Token如果簽名不對(duì)或過(guò)期會(huì)拋異常 Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); request.setAttribute(userRole, claims.get(role)); return true; } }這里有個(gè)容易踩的坑攔截器拋異常全局異常處理器接得住嗎需要注意攔截器preHandle里拋出的異常如果你是注冊(cè)在WebMvcConfigurer里全局異常處理器是能接到的但如果你用HandlerInterceptor實(shí)現(xiàn)類(lèi)里的邏輯拋了異常而你的異常處理器沒(méi)有配置對(duì)前端會(huì)收到亂碼。解決辦法是確保ControllerAdvice生效并且自定義異常類(lèi)里別吞掉堆棧信息。Token如果要實(shí)現(xiàn)用戶(hù)被禁用后立刻失效光靠JWT是做不到的因?yàn)镴WT是無(wú)狀態(tài)的。我提供兩個(gè)方案一是把Token有效期設(shè)短一些比如2小時(shí)配合前端重新登錄機(jī)制二是在user表加一個(gè)token_version字段每次登錄1JWT里存這個(gè)版本號(hào)服務(wù)端校驗(yàn)時(shí)比對(duì)版本號(hào)。方案二在畢設(shè)里屬于亮點(diǎn)功能答辯可以主動(dòng)講。4.3 業(yè)務(wù)接口設(shè)計(jì)的幾個(gè)原則有幾個(gè)接口設(shè)計(jì)的原則我寫(xiě)代碼的時(shí)候一直在用原則一接口返回結(jié)構(gòu)要統(tǒng)一。所有接口都返回ResultT里面包含code、message、data三個(gè)字段。成功時(shí)code200失敗時(shí)code500或者業(yè)務(wù)碼。前端拿到這個(gè)結(jié)構(gòu)統(tǒng)一處理不需要每個(gè)接口單獨(dú)寫(xiě)異常邏輯。原則二列表查詢(xún)千萬(wàn)別只做一個(gè)findAll。分頁(yè)查詢(xún)是標(biāo)配前端傳pageNum和pageSize后端用MyBatis-Plus的Page對(duì)象一行代碼就能搞定分頁(yè)public PageCourseOrder getOrderPage(Long userId, int pageNum, int pageSize) { LambdaQueryWrapperCourseOrder wrapper new LambdaQueryWrapper(); wrapper.eq(userId ! null, CourseOrder::getParentId, userId) .orderByDesc(CourseOrder::getCreateTime); return orderMapper.selectPage(new Page(pageNum, pageSize), wrapper); }LambdaQueryWrapper這個(gè)類(lèi)強(qiáng)烈推薦使用它用lambda表達(dá)式引用列名編譯期就能查出字段名錯(cuò)誤比寫(xiě)字符串parent_id安全得多。原則三修改和刪除要加是本人操作校驗(yàn)。比如老師想取消一個(gè)訂單你得先查出訂單里的teacher_id是不是當(dāng)前登錄用戶(hù)不是就拒絕。這個(gè)邏輯雖然簡(jiǎn)單但很多初學(xué)者會(huì)忘這是數(shù)據(jù)安全的基本底線。5. 前端聯(lián)調(diào)與打包Vue Spring Boot的協(xié)同細(xì)節(jié)5.1 接口約定與聯(lián)調(diào)環(huán)境家教管理系統(tǒng)的前端我一般用Vue 3 Element Plus理由不用多說(shuō)組件成熟、文檔豐富、招聘要求里也常出現(xiàn)。前后端分離開(kāi)發(fā)時(shí)最大的問(wèn)題是跨域。Spring Boot后端解決跨域很簡(jiǎn)單寫(xiě)一個(gè)配置類(lèi)Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8081) // 前端地址 .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }如果你用了Shiro或者Spring Security跨域配置要放行預(yù)檢請(qǐng)求OPTIONS不然瀏覽器先發(fā)個(gè)預(yù)檢請(qǐng)求被攔截你后面真正的請(qǐng)求壓根發(fā)不出去。前后端接口字段的命名也要提前約定好。我習(xí)慣后端返回駝峰命名比如createTime前端直接用不做轉(zhuǎn)換。MySQL里的create_time通過(guò)MyBatis-Plus的駝峰映射自動(dòng)轉(zhuǎn)成createTime所以Java實(shí)體類(lèi)里也是駝峰三層命名一致省去很多煩惱。5.2 Vue打包放進(jìn)Spring Boot的坑與處理很多同學(xué)做完前后端分離老師卻說(shuō)我要一個(gè)能直接跑的東西這時(shí)候就需要把Vue打包后放到Spring Boot里打成單jar運(yùn)行。步驟很簡(jiǎn)單在Vue項(xiàng)目根目錄執(zhí)行npm run build生成dist目錄。把dist里的文件復(fù)制到Spring Boot的src/main/resources/static目錄下。重新打包Spring Bootjava -jar啟動(dòng)后直接訪問(wèn)http://localhost:8080/就能看到前端頁(yè)面。但這里有幾個(gè)坑幾乎必踩坑一Vue路由的history模式會(huì)導(dǎo)致刷新404。如果你用history模式路由訪問(wèn)/order/list刷新頁(yè)面時(shí)Spring Boot會(huì)把請(qǐng)求交給DispatcherServlet找不到對(duì)應(yīng)的/order/list接口就會(huì)返回404。解決辦法是在后端加一個(gè)路由轉(zhuǎn)發(fā)規(guī)則Controller public class RouteController { RequestMapping(value /{path:[^\\.]*}) public String forward() { return forward:/index.html; } }這個(gè)正則[^\\.]*的意思是路徑里不含點(diǎn)號(hào)就轉(zhuǎn)發(fā)到index.html這樣既能放行靜態(tài)資源js/css帶點(diǎn)號(hào)又能解決前端路由404。這個(gè)配置在畢設(shè)答辯里是一個(gè)很能加分的點(diǎn)因?yàn)榇蟛糠滞瑢W(xué)根本不會(huì)遇到這個(gè)問(wèn)題??佣岸苏?qǐng)求的BaseURL要跟部署環(huán)境適配。開(kāi)發(fā)時(shí)前端代理指向http://localhost:8080打包之后前端頁(yè)面和后端同源了如果代碼里還硬編碼了http://localhost:8080部署到服務(wù)器上就廢了。我的做法是在Vue里用環(huán)境變量區(qū)分// .env.development VITE_API_BASE_URL /api // .env.production VITE_API_BASE_URL /api開(kāi)發(fā)時(shí)配置Vite代理轉(zhuǎn)發(fā)生產(chǎn)時(shí)因?yàn)橥此圆恍枰?。這樣代碼里就統(tǒng)一寫(xiě)axios.post(${baseUrl}/order/create)不會(huì)出現(xiàn)環(huán)境切換的麻煩??尤募蟼髀窂絼e用相對(duì)路徑。家教老師上傳資質(zhì)證明教師資格證照片很多同學(xué)會(huì)把文件保存到static/upload/目錄下這在本地跑好好的部署到服務(wù)器上就出問(wèn)題。更好的做法是配置一個(gè)絕對(duì)路徑的上傳目錄通過(guò)application.yml里的file.upload-path配置然后用一個(gè)虛擬路徑映射到它Configuration public class FileConfig implements WebMvcConfigurer { Value(${file.upload-path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath /); } }這樣前端訪問(wèn)/upload/xxx.jpg就能拿到圖片而文件本體存在服務(wù)器某個(gè)固定目錄不會(huì)因?yàn)橹匦麓虬黬ar而丟失。6. 實(shí)測(cè)中踩過(guò)的坑與優(yōu)化建議6.1 Spring Boot版本和依賴(lài)兼容性問(wèn)題這幾個(gè)月我?guī)腿舜罱ōh(huán)境時(shí)遇到最多的報(bào)錯(cuò)九成出在版本上。Spring Boot 2.7.x對(duì)應(yīng)MyBatis-Plus的mybatis-plus-boot-starter要選3.5.x但如果你用Spring Boot 3.x要求MyBatis-Plus 3.5.3以上并且包名會(huì)變從com.baomidou變到com.github.pagehelper這種邊邊角角的兼容問(wèn)題則更隱蔽網(wǎng)上老教程的代碼直接復(fù)制過(guò)來(lái)多半跑不起來(lái)。我的建議是做畢設(shè)就鎖死一套經(jīng)過(guò)驗(yàn)證的組合別追求最新。推薦組合如下組件推薦版本JDK1.8或11Spring Boot2.7.18MyBatis-Plus3.5.3.1MySQL驅(qū)動(dòng)8.0.33Hutool5.8.xJJWT0.11.5Hutool這個(gè)工具庫(kù)我強(qiáng)烈推薦里面封裝了日期處理、加密、文件上傳、驗(yàn)證碼生成等等常用方法能少做很多輪子。JJWT是標(biāo)準(zhǔn)的Java JWT庫(kù)0.11.5版本對(duì)JDK 8友好別去用io.jsonwebtoken:jjwt的舊坐標(biāo)那是另一個(gè)坑。6.2 定時(shí)任務(wù)的實(shí)現(xiàn)與注意事項(xiàng)家教系統(tǒng)里天然需要定時(shí)任務(wù)比如課時(shí)開(kāi)始前2小時(shí)給老師發(fā)提醒訂單超過(guò)24小時(shí)未確認(rèn)自動(dòng)取消月底生成賬單。Spring Boot的定時(shí)任務(wù)用Scheduled注解就夠Component public class OrderAutoCloseTask { Scheduled(cron 0 0 * * * ?) // 每小時(shí)執(zhí)行一次 public void closeExpiredOrders() { // 查詢(xún)狀態(tài)為待確認(rèn)、創(chuàng)建時(shí)間超過(guò)24小時(shí)的訂單批量改為已取消 } }但凡是定時(shí)任務(wù)就避不開(kāi)幾個(gè)細(xì)節(jié)一是cron表達(dá)式別寫(xiě)錯(cuò)。Spring Boot的cron是6段有時(shí)7段秒 分 時(shí) 日 月 周網(wǎng)上抄的表達(dá)式多半是Quartz的7段跑不起來(lái)別慌把首位去掉試試。二是定時(shí)任務(wù)默認(rèn)是單線程串行執(zhí)行的。如果你有多個(gè)定時(shí)任務(wù)默認(rèn)情況下它們是排隊(duì)完成的如果某個(gè)任務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng)會(huì)卡住后面的任務(wù)。要并發(fā)執(zhí)行就配置一個(gè)TaskScheduler并設(shè)置線程池大小。這個(gè)點(diǎn)回答系統(tǒng)如何優(yōu)化性能時(shí)可以作為一個(gè)小亮點(diǎn)。三是線上環(huán)境里定時(shí)任務(wù)要關(guān)注冪等性。比如自動(dòng)取消超時(shí)訂單這個(gè)任務(wù)如果執(zhí)行到一半服務(wù)器重啟了重啟后任務(wù)會(huì)重新執(zhí)行你怎么保證不會(huì)重復(fù)取消我的辦法是任務(wù)執(zhí)行前判斷訂單當(dāng)前狀態(tài)只有在待確認(rèn)狀態(tài)才執(zhí)行取消已經(jīng)取消的跳過(guò)天然冪等。6.3 上線前要做的安全加固畢設(shè)雖然很多只需要在本地演示但如果你想把項(xiàng)目放到服務(wù)器上給老師遠(yuǎn)程看以下安全措施一個(gè)都別省密碼加密禁止明文存密碼BCryptPasswordEncoder起步這個(gè)Spring Security自帶不需要引入全套Security框架單獨(dú)引入spring-security-crypto依賴(lài)即可。SQL注入防堵MyBatis-Plus的LambdaQueryWrapper參數(shù)綁定天然防注入但如果是手寫(xiě)XML里的${}傳參就要小心能用#{}就別用${}。越權(quán)訪問(wèn)防護(hù)JWT里存了role但后端不能只看前端傳的role每個(gè)角色接口的鑒權(quán)必須依賴(lài)攔截器里從Token解析出的角色不能信任前端請(qǐng)求體里的字段。限制敏感接口的請(qǐng)求頻率比如發(fā)送短信驗(yàn)證碼、登錄接口這種加個(gè)簡(jiǎn)單的Redis計(jì)數(shù)器限制頻率。如果項(xiàng)目還沒(méi)引入Redis也可以先用一個(gè)ConcurrentHashMap手動(dòng)計(jì)數(shù)兜底差距區(qū)別只在分布式環(huán)境上。7. 寫(xiě)在最后的項(xiàng)目落地個(gè)人體會(huì)這個(gè)家教管理系統(tǒng)從建表到全部功能跑通我前后花了大概四天。如果只是為自己做三天就夠剩下一天全部消耗在環(huán)境和聯(lián)調(diào)上了。給我的最大感受是寫(xiě)業(yè)務(wù)系統(tǒng)的核心能力不是把代碼敲出來(lái)而是把業(yè)務(wù)關(guān)系理清楚——學(xué)生要不要單獨(dú)建表訂單狀態(tài)怎么流轉(zhuǎn)課時(shí)確認(rèn)之后錢(qián)怎么算這些問(wèn)題想清楚了Spring Boot只是一個(gè)順手的工具代碼寫(xiě)起來(lái)非???。但如果你一上來(lái)就悶頭建工程寫(xiě)CRUD后面改表的代價(jià)會(huì)教你做人。另一個(gè)實(shí)操建議是從中間表入手寫(xiě)代碼不要從頭表寫(xiě)起。我每次都是先做訂單表和課時(shí)記錄表因?yàn)檫@兩張表關(guān)聯(lián)了其他所有表把它們跑通了用戶(hù)表和科目表怎么接都順理成章。最后留一個(gè)可以當(dāng)答辯加分項(xiàng)/擴(kuò)展方向的小提示家教系統(tǒng)的推薦邏輯其實(shí)特別適合做簡(jiǎn)單化處理比如同科目?jī)?yōu)先、同區(qū)域優(yōu)先、價(jià)格從低到高排序就可以用一句SQL完成如果你想再深入一步還可以引入用戶(hù)評(píng)價(jià)分作為排序權(quán)重這就是一個(gè)輕量級(jí)協(xié)同過(guò)濾的雛形。這句話你寫(xiě)在論文結(jié)論部分比空談未來(lái)可以接入大數(shù)據(jù)分析要具體得多也更能體現(xiàn)你真的想過(guò)系統(tǒng)演進(jìn)的方向。