實戰(zhàn)全流程)
又到了一年兩度的畢業(yè)設計選題季托管管理系統(tǒng)這個題目每年都有一大批人做但真正能把前后端串起來、把業(yè)務流程講清楚的并不多。很多同學卡在最基礎的地方微信小程序怎么和后端通信登錄狀態(tài)怎么維持接送記錄和簽到記錄怎么設計才不會亂這篇文章就圍繞一個基于SpringBoot和微信小程序的小學生托管管理系統(tǒng)把從業(yè)務拆解、數(shù)據(jù)庫設計、核心接口實現(xiàn)到小程序端聯(lián)調(diào)、部署運行的完整鏈路捋一遍。內(nèi)容偏實戰(zhàn)適合正在做類似課題、或者想快速入門SpringBoot全棧開發(fā)的同學參考。我會把當時設計時踩過的坑和后來復盤覺得最該注意的點也都寫出來。1. 托管業(yè)務拆解先理清需求再談技術選型1.1 托管班的日常運轉(zhuǎn)到底有哪些事做系統(tǒng)之前我專門跑了兩家托管機構把他們的日常流程捋了一遍。所謂托管其實就是放學后把孩子接到機構里看著寫作業(yè)、管頓飯、等家長來接。這里面有四個核心角色家長、學生、老師、機構管理員。日常動作看起來簡單但拆成系統(tǒng)需求后其實不少早晨或放學后學生到達托管班需要記錄入托時間學生離開時需要記錄離托時間并且要確認是哪個家長接走的家長需要實時看到孩子到?jīng)]到、走沒走、作業(yè)完成狀態(tài)每月收費、繳費記錄、欠費提醒孩子生病或有事不能來家長需要請假機構發(fā)通知比如放假安排、活動通知管理員要能按班級、按時間查看考勤統(tǒng)計。這些需求單獨拎出來每一個都不難但要在同一個系統(tǒng)里串起來并且保證數(shù)據(jù)準確、流程可追溯就需要在設計和實現(xiàn)上下一番功夫。1.2 技術選型SpringBoot與微信小程序為什么是絕配后端選SpringBoot理由很實在生態(tài)成熟、資料多、答辯好講。SpringBoot的自動配置機制讓項目初期搭建成本極低配合MyBatis-Plus單表CRUD基本不用寫SQL開發(fā)效率很高。Java語言的強類型特性也讓多人協(xié)作時更容易保持代碼穩(wěn)定。前端選微信小程序核心原因是家長端的使用門檻為零。托管班的家長群體里很多是爺爺奶奶輩讓他們下載一個獨立App根本不現(xiàn)實但在微信里打開小程序、點一下就能看到孩子狀態(tài)幾乎不需要學習成本。從技術角度說小程序提供了完整的登錄體系、模板消息訂閱能力而且開發(fā)調(diào)試工具很成熟比H5在微信里的體驗更可控。這里我也對比過另一條路線只做一個web管理后臺家長端用微信群人工溝通。結果是老師每天要拍幾十張照片發(fā)群家長信息淹沒在聊天記錄里出了糾紛都找不到依據(jù)。所以最終方案還是管理后臺家長小程序雙端結構。1.3 功能模塊劃分雙端各司其職整個系統(tǒng)的功能模塊劃分大致如下這個劃分也直接決定了后面的表結構和接口設計。模塊核心功能面向角色實現(xiàn)端學生管理學生信息增刪改查、分班、狀態(tài)管理老師/管理員管理后臺小程序班級管理班級信息、師生綁定、容量設置管理員管理后臺小程序考勤簽到入托簽到、離托簽退、記錄查詢老師/家長雙端接送管理接送人白名單、接送記錄確認老師/家長雙端費用管理月費生成、繳費登記、欠費統(tǒng)計管理員/家長雙端請假管理請假申請、審批、缺勤同步家長/老師雙端公告通知發(fā)布公告、消息觸達管理員/家長雙端每個模塊的功能都不復雜難點在于模塊之間的數(shù)據(jù)聯(lián)動。比如請假審批通過后當天的考勤狀態(tài)要自動標記為請假不能算缺勤繳費完成后家長端的費用狀態(tài)要實時刷新。2. 核心表結構設計從業(yè)務反推數(shù)據(jù)庫而不是先畫ER圖2.1 主表設計學生、班級、老師、家長四張核心表很多新手做表結構時喜歡一上來就畫ER圖結果畫出來一堆關系落庫的時候卻不知道怎么處理。我的做法正好反過來把業(yè)務里的每一個功能點列出來反推需要哪些表、哪些字段。這個系統(tǒng)的核心主表有六張student學生信息表字段包括學生姓名、性別、出生日期、班級ID、入學日期、狀態(tài)。特別要注意的字段是status我用了tinyint類型1代表在讀0代表退托這樣退托學生不會影響班級考勤統(tǒng)計。class_info托管班級表包括班級名稱、所屬年級、負責老師ID、容量上限。這里有個業(yè)務細節(jié)一個托管班通常對應一個主帶老師所以直接冗余了teacher_id查詢班級列表時少一次join。teacher教師表包括登錄賬號、密碼BCrypt加密、姓名、手機號、角色。角色字段我用的是普通字符串雖然不夠優(yōu)雅但勝在直觀項目里判斷權限時直接用ADMIN、TEACHER比較。parent_user家長表核心字段是openid這是微信小程序登錄后拿到的唯一標識。第一次通過微信授權登錄時系統(tǒng)會自動創(chuàng)建這條記錄。student_parent_rel學生家長關聯(lián)表這個表單獨說。sign_record簽到記錄表后面詳細說。2.2 學生與家長的多對多關系為什么必須建關聯(lián)表學生和家長之間不是簡單的一對一。一個家庭可能有兩個孩子都在同一個托管班一個孩子也可能同時綁定父親、母親、爺爺奶奶四個監(jiān)護人。如果只在student表里加一個parent_id字段遇到多孩子或多家長的情況就直接崩了。所以這里必須建關聯(lián)表student_parent_rel字段包括自增ID、student_id、parent_id、關系類型爸爸/媽媽/爺爺/奶奶/其他、是否接送授權人。其中是否接送授權人這個字段是我后來加上的因為在接送場景里不是所有綁定家長都有權接走孩子得有一個明確的標記。查詢設計上一個典型的接口邏輯是家長登錄后先查關聯(lián)表拿到所有student_id再查student表列表。這里我用了一條SQL解決SELECT s.* FROM student s INNER JOIN student_parent_rel r ON s.id r.student_id WHERE r.parent_id #{parentId} AND s.status 12.3 簽到記錄與接送記錄兩個動作不能混在一張表里入托簽到和離托接送是兩個完全不同的業(yè)務動作雖然它們都發(fā)生在學生進出托管班這個時間點上但記錄的內(nèi)容和用途差別很大。簽到記錄側(cè)重的是考勤誰、幾點到、幾點走、當天狀態(tài)正常/遲到/請假/缺勤。它面向的是老師和管理員用于統(tǒng)計出勤率。接送記錄側(cè)重的是安全與責任歸屬誰來接的、和學生的關系是什么、哪個老師確認放行的。它面向的更多是安全事故追溯出了問題能查清楚哪一環(huán)有漏洞。我把這兩張表分開設計字段對比如下字段sign_record簽到記錄pickup_record接送記錄學生IDstudent_idstudent_id動作類型sign_type1入托/2離托無本身就是接送行為時間sign_timepickup_time操作人operator_id老師IDconfirm_teacher_id確認老師ID關聯(lián)人員無picker_name、picker_relation接送人姓名與關系備注remarkremark之所以要拆還有一個現(xiàn)實原因簽到記錄一天只有兩條入托離托但接送記錄可能一天有多條比如中午家長臨時接走一小時內(nèi)送回這種特殊場景如果是單表design記錄會非常混亂。3. 后端接口與小程序端的交互設計登錄、鑒權、請求封裝3.1 微信登錄完整流程code換openid再換業(yè)務token微信小程序登錄和傳統(tǒng)賬密登錄最大的區(qū)別是它沒有密碼這個概念。整個流程基于微信的開放能力小程序端調(diào)用wx.login()拿到一個臨時憑證code把code傳給自己的后端接口/api/auth/login后端用code調(diào)用微信的jscode2session接口換回用戶的openid和session_key后端拿著openid查parent_user表存在則直接生成業(yè)務token返回不存在則自動創(chuàng)建新用戶前端把token存到wx.setStorageSync里后續(xù)所有請求都帶這個token。這里有個安全誤區(qū)必須提醒openid的換取必須放在后端完成。有些同學圖方便在小程序端直接請求微信接口換openid這意味著你不得不在小程序代碼里寫死AppSecret而小程序代碼是半公開的AppSecret一旦泄露任何人都能偽造身份。后端核心代碼大致是這樣RestController RequestMapping(/api/auth) public class AuthController { Resource private ParentUserService parentUserService; GetMapping(/login) public Result login(RequestParam String code) { // 1. 調(diào)用微信接口換取openid WxLoginResult wxResult wxService.code2Session(code); if (wxResult.getErrcode() ! 0) { return Result.error(微信登錄失敗 wxResult.getErrmsg()); } // 2. 查詢或創(chuàng)建家長用戶 String openid wxResult.getOpenid(); ParentUser parent parentUserService.findByOpenid(openid); if (parent null) { parent parentUserService.createByOpenid(openid); } // 3. 簽發(fā)業(yè)務token String token JwtUtil.createToken(parent.getId(), parent.getRole()); return Result.ok(token); } }3.2 攔截器鑒權與三種角色的權限控制系統(tǒng)里一共三類使用者管理員、老師、家長。接口不能誰都能調(diào)比如家長不能調(diào)用簽到接口老師不能修改收費標準。我的做法是后端統(tǒng)一加一個AuthInterceptor攔截器所有/api/開頭的請求都會先經(jīng)過它。攔截器從請求頭里拿token校驗簽名和有效期解析出用戶ID和角色放ThreadLocal里供后續(xù)Controller直接取用。Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BizException(401, 未登錄); } // 解析token若過期或非法則拋異常 LoginUser user JwtUtil.parseToken(token); UserContext.set(user); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); // 防止線程池復用導致用戶串號 } }角色控制我用了Spring攔截器的路徑匹配來實現(xiàn)簡潔夠用不需要引入Spring Security那么重的框架。比如/api/teacher/**的請求必須角色為TEACHER或ADMIN/api/admin/**必須為ADMIN。這種配置對一個畢設級別、訪客量不大的系統(tǒng)是合適的真要做大型生產(chǎn)系統(tǒng)再換Security做細粒度權限。3.3 小程序端請求封裝統(tǒng)一域名、token注入與錯誤處理小程序端的請求不能每次都寫一遍wx.request必須封裝。我習慣在utils/request.js里統(tǒng)一管理。const BASE_URL http://192.168.1.100:8080/api const request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method || GET, data: data || {}, header: { Authorization: wx.getStorageSync(token) }, success: (res) { if (res.statusCode 200 res.data.code 200) { resolve(res.data.data) } else if (res.data.code 401) { wx.navigateTo({ url: /pages/login/login }) reject(res.data) } else { wx.showToast({ title: res.data.msg, icon: none }) reject(res.data) } }, fail: (err) { wx.showToast({ title: 網(wǎng)絡異常, icon: none }) reject(err) } }) }) } module.exports { request, BASE_URL }有個細節(jié)真機預覽時BASE_URL絕對不能寫localhost。手機訪問localhost指向的是手機自身不是你開發(fā)用的電腦。要連電腦上起的后端必須把BASE_URL改成電腦在局域網(wǎng)里的IP地址比如http://192.168.1.100:8080/api。開發(fā)模式下微信開發(fā)者工具可以勾選不校驗合法域名但真機預覽需要在詳情設置里打開調(diào)試模式否則請求會被攔掉。4. 幾個容易寫歪的業(yè)務功能接送授權、繳費流水、請假狀態(tài)機4.1 接送人白名單安全紅線怎么用代碼表達接送場景的業(yè)務痛點在于托管機構必須保證孩子不會被陌生人接走。所以設計上我做了接送人白名單機制。家長在小程序端維護接送白名單也就是student_parent_rel表里is_authorized_pickup1的那些記錄。每新增一個接送授權人系統(tǒng)會要求填寫姓名、關系、手機號。老師在小程序端執(zhí)行離托操作時需要在接送人列表里選擇此刻來接的是誰并可以調(diào)起攝像頭拍一張接送人的照片作為留存證據(jù)。這個流程用后端接口表達如下POST /api/pickup/confirm 參數(shù)studentId, pickerId, photoUrl, remark 邏輯 1. 校驗pickerId是否在studentId的白名單里 2. 校驗學生當前狀態(tài)是否為在托 3. 寫入pickup_record表 4. 將學生狀態(tài)置為已離托。第2步的校驗特別重要。如果學生已經(jīng)離托是不能再次執(zhí)行離托操作的否則會出現(xiàn)一個學生被接走兩次的數(shù)據(jù)臟賬。我當時用一個整型狀態(tài)字段current_status來標記學生當下是在托還是離托1表示在托0表示離托簽到時置為1離托時置為0并且通過事務保證這兩步同時成功或同時失敗。4.2 費用流水表不做余額字段只做變動記錄費用模塊一開始我犯過一個設計錯誤想了直接在student表里加一個balance余額字段每次繳費就加每次計費就減。后來發(fā)現(xiàn)這會產(chǎn)生嚴重的數(shù)據(jù)一致性問題——退費、調(diào)價、優(yōu)惠這些操作很難追溯而且并發(fā)時容易算錯。后來我改成了純粹的流水賬設計一張fee_record表只記錄每一筆費用的變動不冗余任何當前余額。需要看某個學生的繳費情況時就查這張表按時間排序。費用記錄表字段如下字段說明id自增主鍵student_id學生IDfee_type費用類型1月托費/2延時費/3餐費/4退費amount變動金額正數(shù)表示應收負數(shù)表示退費status狀態(tài)0待支付/1已支付/2已取消create_time生成時間pay_time支付時間remark備注比如2024年3月托費為什么不做余額字段因為托管班的收費場景相對簡單一個月就一筆托費不像商城那樣有充值、優(yōu)惠、積分等復雜場景流水賬完全能支持所有查詢需求而且沒有任何一致性問題。如果你發(fā)現(xiàn)項目要求里出現(xiàn)了賬戶余額這種概念那才需要單獨考慮錢包表在此之前流水賬夠了。4.3 請假流程一個簡單的狀態(tài)機卻有明確的業(yè)務閉環(huán)請假功能看似只是家長填個表單但它牽涉到考勤統(tǒng)計的聯(lián)動。我把它設計成三態(tài)狀態(tài)機申請中家長提交后狀態(tài)為PENDING已通過老師審核同意后狀態(tài)為APPROVED同時自動生成一條當天的考勤標記狀態(tài)為請假不記入缺勤已駁回老師審核拒絕后狀態(tài)為REJECTED此時家長可以修改日期后重新提交。這個狀態(tài)機的核心在于已通過時對考勤記錄的操作。如果請假審核通過后考勤表里沒有同步標記月底統(tǒng)計出勤率時就會把請假當缺勤家長一投訴就是事故。所以這個聯(lián)動必須放在請假審批通過的同一個事務里不能靠定時任務去補。請假表的表結構里還有一個隱藏字段leave_date記錄的是具體請假日期。因為托管班的考勤按天算一個請假申請只對應某一天不做多日連請。如果家長想請三天假就分別提交三條申請這樣后端邏輯簡單老師審批也清楚。5. 項目跑通的全流程與典型報錯從零環(huán)境到雙端聯(lián)調(diào)5.1 環(huán)境準備清單這個項目后端依賴的核心技術棧是JDK 8或17看SpringBoot版本、Maven 3.6、MySQL 5.7或8.0前端需要微信開發(fā)者工具。JDK版本這塊特別提醒一下SpringBoot 2.x建議用JDK 8SpringBoot 3.x必須用JDK 17。買的畢設代碼如果用的是SpringBoot 2.7.x但本機裝了JDK 17大概率啟動時沒問題但一旦項目里用了javax開頭的包就會報ClassNotFoundException。先把項目里的pom.xml打開看一下spring-boot-starter-parent版本再決定裝哪個JDK別拿到就急著去改環(huán)境變量。MySQL方面建議直接用8.0字符集建庫時指定utf8mb4避免學生姓名里有生僻字時出現(xiàn)亂碼。建庫語句CREATE DATABASE tuition_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;5.2 后端啟動三步走第一步定基礎環(huán)境。JDK配置好JAVA_HOMEMaven配置好settings.xml里的阿里云鏡像不然拉依賴能把人急死。Maven鏡像配置如下這是每個Java后端開發(fā)者必會的操作mirror idaliyunmaven/id mirrorOfcentral/mirrorOf namealiyun maven mirror/name urlhttps://maven.aliyun.com/repository/public/url /mirror第二步建庫和執(zhí)行SQL。項目源碼里一般會帶一個sql目錄里面是完整的建表腳本和初始數(shù)據(jù)。用Navicat直接執(zhí)行就好。如果源碼沒帶SQL文件那要么是作者忘了打包要么是用了MyBatis-Plus的自動建表插件啟動時自動生成但這種情況反而是隱患。第三步改配置文件。打開src/main/resources/application.yml把數(shù)據(jù)庫賬號密碼、微信小程序AppID和AppSecret改掉。這里微信配置如果沒填登錄接口就會報所以你得先去微信公眾平臺注冊一個小程序賬號拿到AppID。個人主體的注冊就能拿到AppID不需要認證。啟動成功后控制臺會出現(xiàn)SpringBoot的啟動日志訪問http://localhost:8080/一般會返回404這其實說明啟動成功了只是沒有配根路徑的Controller而已。5.3 小程序前端配置與真機調(diào)試打開微信開發(fā)者工具導入小程序前端目錄時需要填寫自己的AppID這一點不要用測試號。測試號雖然能編譯但很多接口權限受限。小程序端要改的位置通常只有一個請求封裝文件里的BASE_URL。調(diào)試階段建議使用局域網(wǎng)IP加后端端口。比如電腦IP是192.168.1.100那就把BASE_URL設為http://192.168.1.100:8080/api。在開發(fā)者工具的詳情-本地設置里勾選不校驗合法域名、web-view業(yè)務域名、TLS版本以及HTTPS證書。這個選項是開發(fā)調(diào)試的生命線不勾選的話請求直接返回fail而且錯誤信息藏在控制臺里不夠顯眼很容易讓人誤判成后端問題。真機預覽時手機和電腦必須連同一個局域網(wǎng)Wi-Fi。如果手機能打開網(wǎng)頁但小程序請求還是失敗檢查一下電腦防火墻是不是攔了8080端口臨時關掉Windows防火墻再試一次成功率會大幅提升。5.4 我踩過的三個坑以及對應的排查思路第一個坑LocalDateTime序列化格式問題前端顯示簽到時間時突然變成了平平整整的數(shù)字字符串比如2024-03-01T10:30:00和預期格式差很遠。這本質(zhì)上是SpringBoot默認的Jackson序列化器對LocalDateTime的輸出格式不符合我們的展示需求。處理方式有兩種一是字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)二是全局配置。我推薦在application.yml里統(tǒng)一配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss但這只對java.util.Date生效對LocalDateTime不生效。LocalDateTime還是要加上JsonFormat注解或者引入jackson-datatype-jsr310模塊并自定義序列化配置。最省事的做法是實體類字段上直接標注。第二個坑MyBatis-Plus邏輯刪除與唯一索引的沖突MyBatis-Plus的邏輯刪除是加了一個deleted字段查詢時自動拼上WHERE deleted 0。但如果某張表的某個字段設置了唯一索引當過一年的學生退托被邏輯刪除后第二年重新錄入一個同名同號的轉(zhuǎn)學生就會觸發(fā)唯一索引沖突因為邏輯上刪掉的記錄其實還在表里。解決思路是在業(yè)務上繞開問題比如對學生的唯一索引設計為student_no加class_id組合或使用唯一索引加版本字段。如果畢設代碼已經(jīng)踩到這個坑最簡單的處理方式是把邏輯刪除改成物理刪除畢竟學生數(shù)據(jù)恢復場景在托管系統(tǒng)里很少出現(xiàn)。第三個坑微信code2Session的errcode尤其40029微信登錄第一次聯(lián)調(diào)時一直報錯code換取openid失敗錯誤碼是40029也就是code無效。這個坑的原因是后端拿到的code已經(jīng)被用過了微信的code是一次性的。常見的觸發(fā)場景有兩個前端登錄邏輯寫了重試或者后端接口被重復調(diào)用。比如頁面onLoad時執(zhí)行wx.loginonShow時又執(zhí)行了一次wx.login后一次拿到的code就會讓前一次的失效。排查時可以直接在微信開發(fā)者工具的Network面板里看請求次數(shù)一般立即就能發(fā)現(xiàn)問題。最后再說幾句大實話做這類全棧管理系統(tǒng)最耗時間的永遠不是某個單一功能而是數(shù)據(jù)流轉(zhuǎn)與功能聯(lián)動的設計。表結構沒想清楚就動手寫接口后期一定會在聯(lián)調(diào)返工上付出成倍的代價。先把業(yè)務場景走一遍把所有需要記錄的狀態(tài)和關系列出來再落數(shù)據(jù)庫這步做得越扎實后面寫代碼越快。如果你拿到的就是一套像這樣帶前后端的畢設源碼我建議不要急著直接跑起來看效果先用Navicat把每個表都點開看一眼字段再用微信開發(fā)者工具的調(diào)試器逐行追一遍登錄流程。把這套系統(tǒng)真正弄懂比單純跑通拿到一個結果有用得多答辯時老師隨便提個問題你也能接住。有個小技巧送給你給老師演示系統(tǒng)前把一些典型數(shù)據(jù)做齊——比如一個學生完整的簽到離托記錄、一筆繳費流水、一條請假審批記錄——截好圖放在PPT里。老師不一定有時間看你現(xiàn)場操作但截圖能直觀展示系統(tǒng)的業(yè)務完整性這是你在答辯時最能拿分的部分。