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