設(shè)計與實現(xiàn)詳解)
每年到了畢業(yè)季就有不少同學(xué)私信問我畢設(shè)選題的事情。說實話與其跟風(fēng)做那些重復(fù)度極高的商城系統(tǒng)、圖書管理系統(tǒng)不如做點既有社會話題性、又能把主流技術(shù)棧完整串起來的項目。我最近幫人review過一套“基于SpringBoot的城市垃圾分類管理系統(tǒng)”的源碼整體質(zhì)量不錯而且挺適合做畢設(shè)的。今天就把這套系統(tǒng)的設(shè)計思路、數(shù)據(jù)庫模型、核心實現(xiàn)和部署排錯心得掰開揉碎了講一講。不管你最后是直接用的開源源碼還是準(zhǔn)備自己從零敲一個同款這篇文章都能幫你避開不少彎路。1. 項目概述與選題邏輯1.1 為什么垃圾分類管理適合當(dāng)畢設(shè)先聊聊選題。很多同學(xué)在畢設(shè)選題的時候都卡在“看起來簡單”和“技術(shù)含量夠”之間。垃圾分類管理系統(tǒng)這個題目看起來是給學(xué)校答辯看的但打開來看它其實覆蓋了一套完整業(yè)務(wù)系統(tǒng)該有的所有環(huán)節(jié)用戶端、管理端、數(shù)據(jù)存儲、狀態(tài)流轉(zhuǎn)、甚至還有積分激勵體系。從答辯表現(xiàn)的角度來說這個選題有個天然優(yōu)勢——業(yè)務(wù)模型貼近生活評委老師一看就明白你在做什么不用花大量口舌解釋業(yè)務(wù)背景。更重要的是它的功能擴(kuò)展空間很大你可以在標(biāo)準(zhǔn)CRUD之外加上垃圾分類查詢、預(yù)約上門回收、積分兌換、統(tǒng)計報表等功能每一塊都能拿出來單獨講技術(shù)實現(xiàn)。這套系統(tǒng)的核心價值在于它完成了從“居民不知道垃圾怎么分”到“分完能拿積分換獎品”的完整閉環(huán)。用戶端解決“怎么分”的疑惑后臺解決“分類結(jié)果怎么管理”的訴求積分體系則解決了“讓用戶愿意主動分類”的激勵問題。技術(shù)棧上則涵蓋了SpringBoot、MyBatis Plus、MySQL、Redis、Vue等畢設(shè)高頻技術(shù)屬于那種“看起來不炫技但每塊都踩在點子上”的項目。1.2 系統(tǒng)功能模塊全貌我拿到這套源碼第一件事就是畫了一張功能腦圖。主要角色分三種普通居民用戶、小區(qū)管理員、平臺超級管理員。用戶端有注冊登錄、垃圾分類查詢、預(yù)約回收、積分明細(xì)、個人中心管理端有垃圾類別管理、回收訂單管理、積分規(guī)則配置、公告發(fā)布、數(shù)據(jù)統(tǒng)計看板。這里需要特別提一下“垃圾類別管理”和“垃圾分類查詢”的區(qū)別。很多同學(xué)一開始把這兩個功能混在一起做結(jié)果后臺配置改了前端查詢結(jié)果卻不更新。這套源碼的處理方式是垃圾類別條目獨立成一張表用戶查詢時實時關(guān)聯(lián)后臺每改一條數(shù)據(jù)用戶端下一次搜索立刻就能看到最新結(jié)果不需要重新構(gòu)建緩存。這個細(xì)節(jié)我后面在代碼解析部分會展開講。2. 數(shù)據(jù)庫設(shè)計與核心表結(jié)構(gòu)2.1 四分類業(yè)務(wù)模型怎么落地到表垃圾分類本身有明確的行業(yè)標(biāo)準(zhǔn)可回收物、有害垃圾、廚余垃圾、其他垃圾。不過到了系統(tǒng)設(shè)計里這個“四分類”不能簡單地寫成四個硬編碼字符串否則以后街道想細(xì)化分類比如增加“大件垃圾”“裝修垃圾”你就得改代碼。這套源碼采用了一張字典表 多張業(yè)務(wù)表的策略。字典表存分類的基本信息比如分類名稱、圖標(biāo)、描述、投放要求業(yè)務(wù)表存具體的垃圾條目比如“塑料瓶”“廢電池”“剩菜剩飯”每個條目通過category_id關(guān)聯(lián)到字典表。這樣做的好處是數(shù)據(jù)結(jié)構(gòu)上有主外鍵約束統(tǒng)計報表按分類聚合時SQL寫起來非常順手。值得留意的是預(yù)約回收模塊單獨建了一張訂單表訂單里既有用戶信息和預(yù)約時段也有垃圾的重量估算和回收狀態(tài)。這個設(shè)計把“知識庫查詢”和“業(yè)務(wù)流程”完全隔離開不會因為垃圾條目刪改影響到訂單歷史數(shù)據(jù)。2.2 核心表結(jié)構(gòu)設(shè)計與建表參考看源碼的時候我習(xí)慣先把建表SQL過一遍因為表結(jié)構(gòu)能直接反映開發(fā)者的數(shù)據(jù)建模水平。下面這張表就是核心的用戶表表結(jié)構(gòu)設(shè)計得比較規(guī)范CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主鍵, phone varchar(11) DEFAULT NULL COMMENT 手機(jī)號, password varchar(128) DEFAULT NULL COMMENT 密碼BCrypt加密, nickname varchar(32) DEFAULT NULL COMMENT 昵稱, avatar_url varchar(255) DEFAULT NULL COMMENT 頭像地址, role tinyint(4) DEFAULT 0 COMMENT 角色0普通用戶 1管理員, status tinyint(4) DEFAULT 1 COMMENT 狀態(tài)1正常 0禁用, create_time datetime DEFAULT NULL COMMENT 創(chuàng)建時間, update_time datetime DEFAULT NULL COMMENT 更新時間, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT用戶表;這里有兩個細(xì)節(jié)值得抄作業(yè)。一是role字段用數(shù)字而不是直接用字符串user/admin這樣擴(kuò)展角色的時候不用動表結(jié)構(gòu)。二是密碼字段長度給了128因為用的是BCrypt加密加密后的字符串比MD5長不少如果你還是用char(32)后面存密文會直接報錯。垃圾條目表的設(shè)計也很典型CREATE TABLE garbage_item ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL COMMENT 垃圾名稱, category_id bigint(20) NOT NULL COMMENT 分類ID, description varchar(255) DEFAULT NULL COMMENT 詳細(xì)說明, recycle_method varchar(255) DEFAULT NULL COMMENT 投放/回收方式, status tinyint(4) DEFAULT 1 COMMENT 狀態(tài)1上架 0下架, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_category (category_id), CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES garbage_category (id) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT垃圾條目表;外鍵約束很多人會忽略但我們自己建表最好還是加上。雖然網(wǎng)上有說法說互聯(lián)網(wǎng)公司為了性能不用外鍵可畢設(shè)場景里外鍵的存在反而是加分項——它能證明你懂?dāng)?shù)據(jù)完整性而且答辯時老師很可能問“垃圾類目刪了訂單數(shù)據(jù)怎么辦”這時你可以說使用ON DELETE RESTRICT限制了直接刪除必須先把關(guān)聯(lián)數(shù)據(jù)處理掉這就是一個很好的答辯論據(jù)。預(yù)約回收訂單表我單獨拿出來說因為它是整張業(yè)務(wù)鏈路的主軸CREATE TABLE recycle_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 訂單編號, user_id bigint(20) NOT NULL COMMENT 下單用戶, category_id bigint(20) DEFAULT NULL COMMENT 回收大類, appointment_time datetime DEFAULT NULL COMMENT 預(yù)約上門時間, address varchar(255) DEFAULT NULL COMMENT 回收地址, weight_estimate decimal(10,2) DEFAULT NULL COMMENT 預(yù)估重量(kg), status tinyint(4) DEFAULT 0 COMMENT 狀態(tài)0待接單 1已接單 2已完成 3已取消, points_rewarded int(11) DEFAULT 0 COMMENT 獎勵積分, remark varchar(255) DEFAULT NULL COMMENT 備注, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT回收訂單表;訂單號的生成方式在源碼里是直接用yyyyMMddHHmmss 隨機(jī)數(shù)這種方案的優(yōu)點是簡單直觀不會有并發(fā)主鍵沖突。如果想更嚴(yán)謹(jǐn)一點可以用數(shù)據(jù)庫自增 Redis自增拼接不過對體量不大的系統(tǒng)來說已經(jīng)夠用了。3. 技術(shù)棧與工程結(jié)構(gòu)解析3.1 后端技術(shù)棧為什么這么選SpringBoot 2.x MyBatis Plus MySQL Redis JWT這套組合在畢設(shè)圈里算是“標(biāo)配中的頂配”。選SpringBoot不奇怪它省掉了Spring MVC時代那一大堆XML配置起步快、社區(qū)活躍遇到問題一搜一大把解決方案。MyBatis Plus作為MyBatis的增強(qiáng)工具最大價值就是單表操作不用寫SQL。比如刪除一個條目只需要調(diào)用garbageItemMapper.deleteById(id)。自由組合條件查詢可以用它的LambdaQueryWrapper既安全又能避免字符串字段名寫錯導(dǎo)致運(yùn)行時才報錯的問題。對于畢設(shè)交作業(yè)這個場景來說MyBatis Plus能讓你把時間花在業(yè)務(wù)邏輯上而不是毫無技術(shù)含量的CRUD模板代碼里。Redis在這個項目里當(dāng)了兩類角色一類是緩存比如首頁的垃圾分類熱榜另一類是JWT Token的存儲。講個實際例子后臺修改了用戶狀態(tài)為禁用后已經(jīng)登錄的用戶理論上還能繼續(xù)訪問接口。這種場景靠前端判斷不夠可靠后端在攔截器里每次校驗Token的同時順帶查一下Redis里的用戶狀態(tài)如果已被禁用就直接拒絕。這個小技巧答辯時講出來老師是會點頭的。前端用的是Vue Element UI典型的后臺管理系統(tǒng)組合。因為這套源碼把前后端完全分離了所以開發(fā)時兩邊獨立跑端口聯(lián)調(diào)階段靠CORS配置放行不同源的請求。后面我在部署環(huán)節(jié)會專門說明這個配置細(xì)節(jié)。3.2 工程目錄結(jié)構(gòu)與啟動流程SpringBoot項目看結(jié)構(gòu)基本就能猜出作者的代碼風(fēng)格。這套源碼的工程結(jié)構(gòu)是標(biāo)準(zhǔn)的單模塊分層src/main/java/com/example/garbage ├── config # 配置類跨域、MyBatis Plus分頁等 ├── controller # 接口層 ├── service # 業(yè)務(wù)邏輯層 │ ├── impl # 業(yè)務(wù)實現(xiàn) ├── mapper # 數(shù)據(jù)訪問層 ├── entity # 實體類 ├── dto # 請求/響應(yīng)封裝對象 ├── common # 公共類統(tǒng)一返回結(jié)果、異常處理、常量 ├── utils # 工具類JWT工具、日期工具 └── GarbageApplication.java啟動流程也不復(fù)雜三步走先創(chuàng)建數(shù)據(jù)庫并導(dǎo)入garbage.sql然后修改application.yml里的數(shù)據(jù)庫賬號密碼最后運(yùn)行GarbageApplication.java的main方法。整個過程沒有任何分布式中間件的強(qiáng)依賴單機(jī)就能跑起來這也符合畢設(shè)演示的實際情況。有點需要提醒的源碼里的application.yml有多個配置項比如redis.host、file.upload-path、jwt.secret等別看漏了。有些同學(xué)啟動報錯其實是Redis連不上但日志只給了“連接超時”讓人誤以為是SpringBoot本身的問題白白排查老半天。4. 核心功能實現(xiàn)與代碼細(xì)節(jié)4.1 登錄鑒權(quán)JWT方案登錄模塊看起來簡單但真正動手做過的人都知道坑不少。這套源碼用的是JWT Redis的解決方案用戶登錄成功后后端生成一個Token返回給前端前端在請求頭里帶上Authorization: Bearer token后端寫了一個攔截器統(tǒng)一校驗。生成Token的代碼大致是這樣public String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }這里有幾個點值得細(xì)看。一是簽發(fā)時間setIssuedAt和過期時間setExpiration一定要都設(shè)置有些教程只設(shè)置過期時間導(dǎo)致生成的Token沒有簽發(fā)時間后面排查問題會很被動。二是密鑰secretKey不要硬編碼在代碼里雖然畢設(shè)項目一般不會有人真的攻擊你的服務(wù)但在課上被老師看到你把密鑰寫在常量類里印象分還是會打折的我傾向于放到配置文件里引用。攔截器校驗的邏輯也不復(fù)雜先從請求頭里取Token解析成功后再查Redis是否還在有效期內(nèi)兩步都通過才放行。有些同學(xué)問為什么Token本身已經(jīng)包含了過期時間還要查Redis答案其實我前面提過——為了支持“強(qiáng)制下線”這種主動注銷場景你不用Redis的話后端根本沒有辦法提前作廢一個還沒過期的Token。4.2 垃圾分類查詢與知識庫用戶在前端搜索框輸入“塑料瓶”系統(tǒng)返回該垃圾條目以及對應(yīng)的分類名稱、投放建議和回收方式這個功能就是知識庫的核心。源碼里用的是LambdaQueryWrapper做模糊搜索public ListGarbageItemVO searchGarbage(String keyword) { LambdaQueryWrapperGarbageItem wrapper new LambdaQueryWrapper(); wrapper.like(item - item.getName(), keyword) .and(item - item.getCategoryName() ! null item.getStatus() 1); ListGarbageItem items garbageItemMapper.selectList(wrapper); return convertToVO(items); }like用的是SQL里的%keyword%寫法這個做法對中小型數(shù)據(jù)量完全沒壓力。但如果垃圾條目表到了幾十萬條規(guī)模%keyword%是走不了索引的那時候就該考慮全文檢索方案比如MySQL全文索引或者ES。不過畢設(shè)項目把所有條目全量插進(jìn)去也就幾千條用like完全OK答辯時如果老師問性能你把這個擴(kuò)展思路講出來比單純說“能用”要有力得多。搜索接口的響應(yīng)設(shè)計成統(tǒng)一返回結(jié)構(gòu)我用過一次之后發(fā)現(xiàn)這個習(xí)慣還挺重要{ code: 200, message: success, data: { list: [...], total: 5 } }前端的Vue里對response.code做了判斷非200統(tǒng)一彈ElMessage提示錯誤。這種統(tǒng)一返回結(jié)構(gòu)減少了前后端聯(lián)調(diào)的溝通成本也讓接口風(fēng)格看起來更專業(yè)我在自己日常的項目里也一直沿用這個模式。4.3 預(yù)約回收與積分閉環(huán)預(yù)約回收這個環(huán)節(jié)是區(qū)分“課設(shè)CRUD”和“更像真實業(yè)務(wù)系統(tǒng)”的分水嶺。用戶選擇垃圾類別、填寫上門時間地址、提交訂單后訂單狀態(tài)為“待接單”管理員在后臺把訂單改成“已接單”然后等實際上門完成后再改成“已完成”。這里面的狀態(tài)流轉(zhuǎn)源碼里不是簡單地用if else硬寫而是提取了一個訂單狀態(tài)枚舉類各個狀態(tài)之間還做了合法流轉(zhuǎn)校驗。這個設(shè)計值得學(xué)習(xí)的地方在于它把訂單狀態(tài)從“字符串魔改”變成了“類型安全”的對象。比如你要從“待接單”直接跳到“已完成”系統(tǒng)會直接拒絕因為枚舉里定義的狀態(tài)機(jī)根本不允許這個跳躍。畢設(shè)項目能做出這種細(xì)節(jié)說明你對業(yè)務(wù)有認(rèn)真思考。積分發(fā)放的邏輯放在訂單被標(biāo)記為“已完成”的時候觸發(fā)而不是在“已接單”階段就發(fā)放。這個細(xì)節(jié)很關(guān)鍵因為它保證了用戶真正把垃圾交出去之后才能拿到獎勵。積分的計算規(guī)則是估算重量大于0就按重量 * 權(quán)重?fù)Q算默認(rèn)情況下每公斤計10分。源碼里把權(quán)重直接做成了一張配置表方便管理員在后臺隨時調(diào)整。這套配置化的思路也可以套用在簽到積分、評論積分等場景里。4.4 管理后臺與統(tǒng)計報表管理后臺是整個系統(tǒng)的“面子工程”也是答辯老師最喜歡演示的部分。源碼里集成了ECharts用幾個簡單的折線圖、柱狀圖和餅圖來展示各小區(qū)回收單量的趨勢、垃圾類別的占比、用戶積分排行等數(shù)據(jù)。統(tǒng)計接口的實現(xiàn)很有意思不是到業(yè)務(wù)庫里做復(fù)雜的多表聯(lián)查而是單獨建了一張daily_statistics表每天定時任務(wù)跑一次把昨天的訂單數(shù)、總重量、分類統(tǒng)計都預(yù)處理好。前端查詢的時候直接讀這張表性能自然快。這個設(shè)計在真實企業(yè)項目里就是“日報表預(yù)處理”在畢設(shè)里用上它等于告訴老師你理解“讀多寫少的數(shù)據(jù)可以提前聚合”這個優(yōu)化原則。ECharts前端配置也不復(fù)雜Vue里在mounted鉤子里調(diào)用后端接口拿到數(shù)據(jù)后直接setOption渲染圖表。唯一提醒的是圖表容器的寬度高度一定要通過style指定我曾經(jīng)見過學(xué)生把div寬度寫成100%結(jié)果圖表渲染出來只有一條線怎么查都查不出來原因后來發(fā)現(xiàn)是父容器沒給高度導(dǎo)致ECharts初始化時拿到的尺寸是0。5. 本地部署與踩坑實錄5.1 環(huán)境準(zhǔn)備與配置清單部署這套系統(tǒng)的硬件門檻很低一臺8G內(nèi)存的筆記本電腦就夠。軟件環(huán)境方面建議統(tǒng)一用這些版本能最大程度避免版本兼容問題依賴項推薦版本說明JDK8 或 11Spring Boot 2.x 必須用 8別用21除非你改了全套依賴Maven3.6.x3.8以上也可以但鏡像源需要配好MySQL5.7 或 8.0建庫用utf8mb4否則中文可能亂碼Redis6.x默認(rèn)端口6379密碼可留空Node.js12~16前端Vue項目npm install用的application.yml里最重要的幾個配置項如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/garbage_db?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 jwt: secret: your-secret-key expire-hours: 24有同學(xué)反映MySQL8.0連接時提示Public Key Retrieval is not allowed這是因為連接串里少了allowPublicKeyRetrievaltrue。JDBC寫法如下url: jdbc:mysql://localhost:3306/garbage_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue原來是MySQL8默認(rèn)使用caching_sha2_password認(rèn)證客戶端第一次連接時想拿服務(wù)端公鑰結(jié)果被安全策略擋住。加上這個參數(shù)就解決了。5.2 常見部署報錯對照表這套系統(tǒng)我在不同電腦上跑過多次也幫人遠(yuǎn)程看過不少錯誤日志大部分問題集中在下面這幾類報錯現(xiàn)象根本原因解決辦法Failed to configure a DataSource數(shù)據(jù)庫連接配置錯誤或驅(qū)動缺失檢查application.yml的url/賬號密碼Table garbage_db.xxx doesnt exist建庫導(dǎo)表不完整用Navicat或命令行重新執(zhí)行g(shù)arbage.sqlERR Client sent AUTH, but no password is setRedis有密碼但沒配置在配置里加上redis.password或清掉本地Redis密碼端口被占用: 8080其他程序占了端口改server.port或查進(jìn)程殺掉npm run serve報Node Sass/ Sass相關(guān)錯誤Node版本和sass-loader不兼容換成Node14或者重裝node-sass端口占用這個問題我多說一句。Windows下可以先netstat -ano | findstr :8080找到PID后到任務(wù)管理器結(jié)束進(jìn)程。macOS/Linux用lsof -i:8080。這種情況在實驗室電腦上特別常見因為上一屆學(xué)長可能跑過別的SpringBoot項目沒關(guān)干凈。5.3 前后端聯(lián)調(diào)時的跨域配置前后端分離后前端跑在http://localhost:5173后端跑在http://localhost:8080兩個端口不同瀏覽器默認(rèn)會攔截非同源的Ajax請求。這時候需要在后端配置CORS。源碼里的做法是寫一個WebMvcConfigurer配置類Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }要注意的是allowedOriginPatterns(*)和allowCredentials(true)搭配是SpringBoot 2.4以后允許的寫法早期的allowedOrigins(*)在這種組合下會報錯。前后端跑在一個服務(wù)器上部署時這個跨域配置可以去掉改成Nginx統(tǒng)一轉(zhuǎn)發(fā)效果更好。6. 從畢設(shè)源碼到答辯加分拿到一套能跑的源碼只是起點真正拉開分差的是你對代碼的理解深度。我建議你在答辯前把這幾條鏈路親手走一遍注冊用戶 → 搜索垃圾條目 → 提交預(yù)約回收 → 管理員后臺接單 → 標(biāo)記完成 → 用戶積分變化 → 積分明細(xì)展示。這條鏈路從頭到尾能串起來說明你理解了系統(tǒng)的數(shù)據(jù)流任何一環(huán)答不上來都容易露怯。一個比較實用的小技巧把源碼里的一些關(guān)鍵表比如recycle_order的ER關(guān)系、狀態(tài)機(jī)流轉(zhuǎn)圖、系統(tǒng)架構(gòu)圖提前畫好打印成紙質(zhì)版帶去答辯現(xiàn)場。老師提問時你直接指著圖講既省時間又顯得專業(yè)。很多同學(xué)只顧著把代碼跑起來卻忽略“可視化表達(dá)”這件事等于白丟了一部分印象分。最后一個建議如果你打算在源碼基礎(chǔ)上改功能優(yōu)先改“積分規(guī)則配置”和“統(tǒng)計報表”這兩個模塊。因為這兩個地方最容易被老師追問“為什么這樣設(shè)計”而你也最容易結(jié)合自己的改動說出真實思考。比如給積分規(guī)則增加一個“按投放次數(shù)加權(quán)”的參數(shù)前端再配合改一個表單工作量不大但講出來的效果完全不一樣。跑通代碼只是開始把每條數(shù)據(jù)流、每個決策的前因后果講明白才是畢設(shè)真正想考察的東西。希望這篇拆解能幫你少走幾步彎路。