W生簽到系統(tǒng)實戰(zhàn):從數(shù)據(jù)庫設計到定位簽到與部署避坑)
簡介這份資源是面向高校計算機相關專業(yè)學生與Java開發(fā)學習者的畢業(yè)設計/課程設計完整項目包主題為基于微信小程序的學生簽到系統(tǒng)后端采用SSM框架實現(xiàn)。項目圍繞簽到管理場景覆蓋學生微信授權登錄、教師創(chuàng)建簽到任務、一鍵與二維碼簽到、簽到記錄查詢、消息提醒、師生權限區(qū)分以及簽到率與遲到曠課統(tǒng)計等模塊適合作為畢設選題參考或SSM與小程序聯(lián)調(diào)的練手案例。壓縮包共1087個文件約16.02MB包含120個Java后端源碼、137個Vue前端頁面、137個JS腳本以及44個wxss與43個wxml小程序頁面文件另有sql建表腳本、properties配置、json數(shù)據(jù)與png、svg等界面素材前后端與數(shù)據(jù)庫結(jié)構相對完整。目前已有132人學習下載。讀者可據(jù)此梳理簽到系統(tǒng)的業(yè)務分層與接口設計參考權限控制、統(tǒng)計分析與消息通知的實現(xiàn)思路并借助現(xiàn)成目錄結(jié)構快速搭建運行環(huán)境、對照排查配置問題。1. 從一份 .rar 說起學生簽到系統(tǒng)為什么還在用 SSM 微信小程序每年畢業(yè)季計算機相關專業(yè)的畢設題目里總有一批“學生簽到系統(tǒng)”反復出現(xiàn)而技術棧幾乎清一色是SSM 框架 微信小程序。很多人第一反應是“這玩意兒不是爛大街了嗎”但真正動手做過的人會發(fā)現(xiàn)它恰好卡在一個很微妙的位置業(yè)務邏輯簡單到能講清楚工程鏈路又完整到能覆蓋從數(shù)據(jù)庫設計、后端接口、小程序端交互到部署聯(lián)調(diào)的全流程。對于想快速跑通一個“能演示、能答辯、能二次開發(fā)”的完整項目的人來說這套組合的性價比其實很高。這份標題里的.rar壓縮包通常包含后端 SSM 工程、小程序前端源碼、SQL 建表腳本和一份說明文檔。它解決的核心問題是教師發(fā)布簽到任務學生在微信小程序端完成定位簽到或數(shù)字簽到后臺實時統(tǒng)計出勤情況。適合的人群很明確——正在做畢設的學生、需要快速搭一套內(nèi)部考勤原型的開發(fā)者以及想拿一個完整項目練手 SSM 與小程序聯(lián)調(diào)的人。接下來我會按“先跑通、再改對、最后避坑”的順序把這條鏈路拆開講清楚。2. 把 SSM 后端跑起來從建表到第一個簽到接口2.1 數(shù)據(jù)庫設計三張核心表撐起整個簽到邏輯拿到壓縮包后不要急著打開 IDE 跑代碼。先看 SQL 腳本因為簽到系統(tǒng)的所有業(yè)務都圍繞三張表展開用戶表、簽到任務表、簽到記錄表。很多同學跑不起來項目根因就是表結(jié)構沒吃透字段對不上。常見做法是先用 Navicat 或命令行把庫建好字符集統(tǒng)一用utf8mb4否則學生姓名里的生僻字會變成問號。下面是我一般會先執(zhí)行的建表語句字段命名和索引都按實際查詢場景做了調(diào)整-- 用戶表同時存教師和學生用 role 區(qū)分 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) DEFAULT NULL COMMENT 微信openid學生端登錄用, username VARCHAR(32) NOT NULL COMMENT 學號或工號, password VARCHAR(64) NOT NULL COMMENT MD5加密存儲, real_name VARCHAR(32) NOT NULL, role TINYINT NOT NULL DEFAULT 0 COMMENT 0學生 1教師, class_id INT DEFAULT NULL COMMENT 學生所屬班級, UNIQUE KEY uk_username (username), KEY idx_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 簽到任務表一次簽到活動對應一條記錄 CREATE TABLE sign_task ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_id INT NOT NULL, title VARCHAR(128) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, sign_type TINYINT NOT NULL DEFAULT 1 COMMENT 1普通簽到 2定位簽到 3數(shù)字簽到, sign_code VARCHAR(8) DEFAULT NULL COMMENT 數(shù)字簽到碼, status TINYINT DEFAULT 1 COMMENT 1進行中 0已結(jié)束, KEY idx_teacher (teacher_id), KEY idx_time (start_time,end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 簽到記錄表學生每簽一次寫一條用聯(lián)合唯一索引防重復 CREATE TABLE sign_record ( id INT PRIMARY KEY AUTO_INCREMENT, task_id INT NOT NULL, student_id INT NOT NULL, sign_time DATETIME DEFAULT CURRENT_TIMESTAMP, latitude DECIMAL(10,7) DEFAULT NULL, longitude DECIMAL(10,7) DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT 1正常 2遲到 3補簽, UNIQUE KEY uk_task_student (task_id,student_id), KEY idx_student (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;這段 SQL 里有兩個參數(shù)值得單獨說。uk_task_student這個聯(lián)合唯一索引是防重復簽到的第一道防線比在 Java 層用if判斷可靠得多因為并發(fā)場景下先查后插必然出問題。sign_type字段決定了后續(xù)接口的分支邏輯定位簽到需要額外存經(jīng)緯度數(shù)字簽到需要校驗sign_code普通簽到則只記錄時間。索引方面idx_time是為了教師端按時間段篩選任務時走索引不然數(shù)據(jù)量上來后列表查詢會明顯變慢。2.2 SSM 分層與簽到接口實現(xiàn)SSM 的經(jīng)典分層是 Controller → Service → Mapper配置文件包括applicationContext.xml、spring-mvc.xml、mybatis-config.xml和jdbc.properties。壓縮包里的配置大概率用的是 MySQL 5.7 驅(qū)動如果你本地是 MySQL 8需要把驅(qū)動類改成com.mysql.cj.jdbc.Driver并在 JDBC URL 后面加上serverTimezoneAsia/Shanghai否則啟動就報時區(qū)錯誤。簽到接口是整個后端最核心的一段代碼它要處理“任務是否在有效期內(nèi)”“學生是否已簽到”“簽到類型對應的校驗邏輯”三件事。我一般會把校驗放在 Service 層Controller 只做參數(shù)接收和結(jié)果包裝Service public class SignServiceImpl implements SignService { Autowired private SignTaskMapper signTaskMapper; Autowired private SignRecordMapper signRecordMapper; Override Transactional(rollbackFor Exception.class) public Result doSign(Integer taskId, Integer studentId, String signCode, BigDecimal lat, BigDecimal lng) { // 1. 查任務判斷是否存在且在有效期內(nèi) SignTask task signTaskMapper.selectById(taskId); if (task null) { return Result.fail(簽到任務不存在); } Date now new Date(); if (now.before(task.getStartTime()) || now.after(task.getEndTime())) { return Result.fail(不在簽到時間范圍內(nèi)); } // 2. 防重復先查一次數(shù)據(jù)庫唯一索引兜底 SignRecord exist signRecordMapper.selectByTaskAndStudent(taskId, studentId); if (exist ! null) { return Result.fail(請勿重復簽到); } // 3. 按簽到類型分支校驗 if (task.getSignType() 3) { if (!task.getSignCode().equals(signCode)) { return Result.fail(簽到碼錯誤); } } // 4. 寫入記錄定位簽到才存經(jīng)緯度 SignRecord record new SignRecord(); record.setTaskId(taskId); record.setStudentId(studentId); record.setSignTime(now); if (task.getSignType() 2) { record.setLatitude(lat); record.setLongitude(lng); } signRecordMapper.insert(record); return Result.ok(簽到成功); } }Transactional注解在這里是必須的因為“查重 插入”是一個原子操作雖然唯一索引能兜底但事務能保證異常時回滾干凈。sign_type的判斷順序也有講究先判數(shù)字簽到再判定位簽到最后走普通簽到這樣擴展新類型時只需要加分支不用動主流程。參數(shù)lat和lng用BigDecimal而不是double是因為經(jīng)緯度精度要求高double在序列化時容易出現(xiàn)精度丟失導致定位簽到算距離時偏差幾十米。2.3 用 Postman 驗證接口再動小程序后端寫完不要直接開小程序模擬器先用 Postman 把接口跑通。啟動 Tomcat 后請求http://localhost:8080/sign/dosignBody 用 form-data 傳taskId、studentId、signCode。如果返回{code:200,msg:簽到成功}說明后端鏈路沒問題。這一步能幫你把“后端問題”和“前端問題”隔離開不然聯(lián)調(diào)時兩邊報錯排查起來就是黑匣子。常見的一個翻車點是中文亂碼。如果返回的msg是問號檢查spring-mvc.xml里有沒有配StringHttpMessageConverter的supportedMediaTypes為text/plain;charsetUTF-8以及 Tomcat 的server.xml里Connector有沒有加URIEncodingUTF-8。這兩個地方漏一個中文就廢了。3. 微信小程序端登錄、簽到頁與請求封裝3.1 微信小程序登錄獲取手機號與 openid 綁定小程序端的登錄流程和普通網(wǎng)頁完全不同它依賴wx.login拿到的臨時code再傳給后端換openid。很多壓縮包里的代碼只做了wx.login就完事沒有把openid和sys_user表關聯(lián)起來導致學生換設備后簽到記錄對不上。正確的做法是后端提供一個/wx/login接口接收code調(diào)用微信的jscode2session接口換回openid然后查sys_user表如果openid不存在就綁定當前登錄的學號。// 小程序端登錄邏輯 wx.login({ success(res) { if (res.code) { wx.request({ url: http://localhost:8080/wx/login, method: POST, data: { code: res.code, username: 2021001 }, success(r) { // 后端返回 openid 和用戶信息存到全局 getApp().globalData.userInfo r.data.data; wx.setStorageSync(token, r.data.data.token); } }); } } });這里有個參數(shù)要注意jscode2session需要appid和secret這兩個值放在后端配置文件里不要寫在小程序端否則泄露后別人能冒充你的小程序。另外wx.login的code只能用一次五分鐘內(nèi)有效所以后端換openid失敗時要提示用戶重新登錄而不是反復用同一個code重試。3.2 簽到頁的交互與定位授權簽到頁通常有一個大按鈕點擊后根據(jù)任務類型彈出不同交互普通簽到直接提交數(shù)字簽到彈輸入框定位簽到先調(diào)wx.getLocation。定位授權是小程序里最容易踩坑的地方因為用戶一旦拒絕后續(xù)再調(diào)wx.getLocation會直接走fail回調(diào)不會再次彈窗。// 定位簽到先檢查授權再獲取位置 wx.getSetting({ success(res) { if (!res.authSetting[scope.userLocation]) { wx.authorize({ scope: scope.userLocation, success() { getLocationAndSign(); }, fail() { wx.showModal({ title: 需要定位權限, content: 請在設置中開啟定位后重新簽到, success(m) { if (m.confirm) wx.openSetting(); } }); } }); } else { getLocationAndSign(); } } }); function getLocationAndSign() { wx.getLocation({ type: gcj02, success(pos) { wx.request({ url: http://localhost:8080/sign/dosign, method: POST, data: { taskId: 1, studentId: 1001, latitude: pos.latitude, longitude: pos.longitude }, success(r) { wx.showToast({ title: r.data.msg }); } }); } }); }type: gcj02這個參數(shù)必須寫因為微信默認返回的是wgs84坐標系而國內(nèi)地圖和距離計算通常用gcj02不統(tǒng)一坐標系會導致簽到距離算出來偏差幾百米。wx.openSetting只能在用戶主動點擊后調(diào)用所以要用showModal引導不能直接跳設置頁。3.3 請求封裝與頂部導航欄高度適配小程序里如果每個頁面都寫一遍wx.request后期改域名或加 token 會改到崩潰。我一般會封裝一個request.js統(tǒng)一處理 baseURL、token 注入和錯誤提示。另外微信小程序頂部導航欄高度在不同機型上不一樣自定義導航欄時需要用wx.getSystemInfoSync拿狀態(tài)欄高度再算上膠囊按鈕的位置否則標題會被劉海擋住。// utils/request.js const BASE_URL http://localhost:8080; function request(options) { const token wx.getStorageSync(token); return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Authorization: token || }, success(res) { if (res.data.code 200) resolve(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 };封裝后頁面里只需要request({ url: /sign/dosign, method: POST, data: {...} })token 自動帶上錯誤統(tǒng)一提示。導航欄高度適配的常見做法是statusBarHeight從系統(tǒng)信息里取導航欄內(nèi)容高度固定 44px總高度就是兩者之和然后把這個值設給自定義導航欄的padding-top。4. 聯(lián)調(diào)與部署讓簽到系統(tǒng)真正跑在手機上4.1 本地聯(lián)調(diào)關閉域名校驗與真機調(diào)試小程序開發(fā)工具默認校驗合法域名本地localhost不在白名單里所以要在“詳情 → 本地設置”里勾選“不校驗合法域名”。真機調(diào)試時手機和電腦必須在同一個局域網(wǎng)把BASE_URL改成電腦的局域網(wǎng) IP比如http://192.168.1.100:8080同時確保防火墻放行 8080 端口。如果真機上請求超時先檢查電腦防火墻再檢查 Tomcat 是否綁定了0.0.0.0而不是127.0.0.1。Tomcat 默認監(jiān)聽所有網(wǎng)卡但有些同學改過server.xml里的address屬性就會導致只有本機能訪問。4.2 部署到服務器打包與數(shù)據(jù)庫遷移正式部署時后端打成war包丟進 Tomcat 的webapps目錄數(shù)據(jù)庫用mysqldump導出再導入服務器。小程序端需要把BASE_URL改成正式域名并在微信公眾平臺配置request合法域名。注意域名必須備案且支持 HTTPS否則小程序正式版無法請求。# 導出本地數(shù)據(jù)庫 mysqldump -u root -p sign_db sign_db.sql # 服務器上導入 mysql -u root -p sign_db sign_db.sql # 打包后端Maven 項目 mvn clean package -DskipTests # 把 war 包復制到 Tomcat cp target/sign.war /usr/local/tomcat/webapps/-DskipTests在部署時常用因為單元測試可能依賴本地環(huán)境服務器上跑會失敗。但第一次部署建議先不加這個參數(shù)確認測試能過再跳過不然可能把編譯錯誤帶到線上。4.3 簽到距離校驗的服務端實現(xiàn)定位簽到如果只存經(jīng)緯度不校驗距離學生可以在宿舍簽教室的到。服務端需要根據(jù)教師設置的簽到點和允許半徑用 Haversine 公式算球面距離。這個邏輯放在 Service 層和簽到主流程分開方便單獨測試。// 計算兩點間距離單位米 private static final double EARTH_RADIUS 6371000; public static double getDistance(double lat1, double lng1, double lat2, double lng2) { double radLat1 Math.toRadians(lat1); double radLat2 Math.toRadians(lat2); double deltaLat radLat1 - radLat2; double deltaLng Math.toRadians(lng1) - Math.toRadians(lng2); double a Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) Math.cos(radLat1) * Math.cos(radLat2) * Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2); double c 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return EARTH_RADIUS * c; }EARTH_RADIUS用 6371000 米是通用近似值誤差在可接受范圍內(nèi)。Math.toRadians把角度轉(zhuǎn)弧度因為三角函數(shù)入?yún)⑹腔《?。算出來的距離和教師設置的allow_radius比較超過就拒絕簽到。這個校驗必須放服務端小程序端的定位可以被模擬不能信任客戶端傳來的坐標。5. 避坑與排查簽到系統(tǒng)上線前必須過的五道坎5.1 現(xiàn)象學生點擊簽到?jīng)]反應控制臺報 400原因通常是請求參數(shù)類型不匹配。小程序端wx.request默認content-type是application/json而后端 Controller 用RequestParam接收Spring 解析不了 JSON body 里的字段。解決方式有兩種要么后端改用RequestBody接收一個 DTO要么小程序端把header改成application/x-www-form-urlencoded。我一般選后者改動最小。5.2 現(xiàn)象簽到記錄里出現(xiàn)同一學生兩條數(shù)據(jù)原因是并發(fā)請求下“先查后插”失效兩個請求同時查到“未簽到”然后都執(zhí)行插入。雖然uk_task_student唯一索引能攔住第二條但會拋DuplicateKeyException如果沒捕獲前端看到的是 500 錯誤而不是“請勿重復簽到”。解決方式是在 Service 里try-catch這個異常返回友好提示同時前端按鈕點擊后立即置灰防止用戶連點。5.3 現(xiàn)象定位簽到距離算出來總是差幾百米坐標系不統(tǒng)一導致的。微信wx.getLocation默認返回wgs84而教師設置的簽到點可能是從地圖上取的gcj02坐標。兩者直接算距離必然偏。解決方式是統(tǒng)一用gcj02小程序端type參數(shù)寫死gcj02教師端設置簽到點時也用同一坐標系。5.4 現(xiàn)象小程序真機上請求后端超時開發(fā)工具正常開發(fā)工具走的是電腦網(wǎng)絡真機走的是手機網(wǎng)絡。常見原因是電腦防火墻攔了 8080 端口或者手機和電腦不在同一網(wǎng)段。排查順序先用手機瀏覽器訪問http://電腦IP:8080如果能打開說明網(wǎng)絡通問題在小程序配置如果打不開先關防火墻再試。另外有些公司 WiFi 開啟了 AP 隔離設備之間不能互訪這種情況只能換熱點。5.5 現(xiàn)象部署到服務器后中文全部亂碼數(shù)據(jù)庫字符集、JDBC URL、Tomcat 編碼三處只要有一處不是utf8mb4就會亂。排查時先看數(shù)據(jù)庫SHOW VARIABLES LIKE character%再看 JDBC URL 有沒有characterEncodingutf8最后看 Tomcat 的URIEncoding。三處都對齊后重啟 Tomcat亂碼基本消失。6. 進階技巧把簽到系統(tǒng)做成能復用的考勤底座跑通基礎簽到后如果想讓這個項目在答辯或?qū)嶋H使用中更有說服力可以往兩個方向加東西。第一個是簽到統(tǒng)計導出教師端按班級導出 Excel用 Apache POI 或者 EasyExcel 都行EasyExcel 內(nèi)存占用更低適合數(shù)據(jù)量大的場景。第二個是簽到提醒用微信訂閱消息在簽到開始前推一條通知給學生這個需要在小程序后臺申請模板 ID后端調(diào)subscribeMessage.send接口。// 用 EasyExcel 導出簽到記錄 public void exportSignRecords(HttpServletResponse response, Integer taskId) throws IOException { ListSignRecordVO list signRecordMapper.selectByTask(taskId); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); String fileName URLEncoder.encode(簽到記錄, UTF-8); response.setHeader(Content-Disposition, attachment;filename fileName .xlsx); EasyExcel.write(response.getOutputStream(), SignRecordVO.class) .sheet(簽到明細) .doWrite(list); }Content-Disposition里的文件名必須URLEncoder.encode否則中文文件名在部分瀏覽器上會變成亂碼。EasyExcel.write的第二個參數(shù)是映射的 VO 類字段上加ExcelProperty(學號)注解就能控制表頭。還有一個容易被忽略的點簽到系統(tǒng)的時區(qū)。服務器如果是 UTC 時間而sign_task的start_time存的是北京時間比較時就會差 8 小時。我一般會在 JDBC URL 里加serverTimezoneAsia/Shanghai并且 JVM 啟動參數(shù)加-Duser.timezoneAsia/Shanghai雙保險。最后說一個我自己的習慣每次改完簽到邏輯先用 Postman 跑一遍正常簽到、重復簽到、超時簽到、錯誤簽到碼四個用例再開小程序點一遍。這四個用例覆蓋了 90% 的線上問題比漫無目的地翻日志快得多。希望幫到你。本文還有配套的精品資源點擊獲取