生積分管理系統(tǒng):從Excel到自動化審核)
簡介這是一套面向?qū)W院學(xué)生管理部門的積分管理系統(tǒng)源碼工程適合計算機專業(yè)學(xué)生、Java Web開發(fā)者及教務(wù)管理人員用于課程設(shè)計、畢業(yè)設(shè)計或二次開發(fā)。系統(tǒng)圍繞積分規(guī)則設(shè)定、積分記錄、查詢排名、獎懲觸發(fā)、報表分析與權(quán)限管理等場景展開將學(xué)生行為、學(xué)業(yè)表現(xiàn)與活動參與度量化評價幫助減輕人工統(tǒng)計負(fù)擔(dān)。壓縮包共364個文件約1.83MB以110個class與110個java源文件構(gòu)成核心業(yè)務(wù)邏輯72個jsp頁面承載前端交互另含55個gif界面素材、4個jar依賴庫、4個properties配置及project、xml等工程描述文件結(jié)構(gòu)完整便于導(dǎo)入部署。已有622人學(xué)習(xí)下載。讀者可從中獲得完整的積分管理實現(xiàn)思路、數(shù)據(jù)庫與Servlet分層結(jié)構(gòu)、頁面與權(quán)限控制參考以及可運行的工程骨架適合對照學(xué)習(xí)Java Web項目開發(fā)與教務(wù)積分模塊設(shè)計。1. 從一張 Excel 積分表說起這套學(xué)生積分管理系統(tǒng)到底能干什么每年評獎學(xué)金、推優(yōu)入黨、社團(tuán)評優(yōu)那幾周總能看到某高校輔導(dǎo)員在群里發(fā)一張 Excel讓各班班長把學(xué)生加分項填進(jìn)去然后收上來一堆格式各異的表格再手動合并、去重、核對。我見過最離譜的一次一個學(xué)院 800 多人的積分表光核對加分憑證就耗了整整三天最后還是有人漏加了兩個競賽獎項。這套「學(xué)院學(xué)生積分管理系統(tǒng)」要解決的就是把這個過程從「人肉 Excel 流水線」變成「規(guī)則配置 自動計算 可追溯審核」的線上流程。它適合兩類人一類是學(xué)院里負(fù)責(zé)學(xué)生事務(wù)、被積分統(tǒng)計折磨過的老師或?qū)W生助理另一類是拿到這套源碼、想照著做一套類似審批積分系統(tǒng)的開發(fā)者。項目本身不復(fù)雜但麻雀雖小用戶、角色、積分規(guī)則、審核流、統(tǒng)計導(dǎo)出這幾塊該有的都有拿來練手或者直接改改上線都夠用。2. 技術(shù)選型與目錄結(jié)構(gòu)一套典型的后臺管理系統(tǒng)骨架2.1 為什么是這套技術(shù)棧這類學(xué)院內(nèi)部系統(tǒng)有個很現(xiàn)實的特點用戶量不大一個學(xué)院撐死幾千人但角色多、權(quán)限細(xì)、要能快速改需求。所以技術(shù)選型上主流做法是后端用 Spring Boot 或類似的 MVC 框架前端用 Vue Element UI 這種后臺管理模板數(shù)據(jù)庫 MySQL緩存可選 Redis。這套組合的好處是生態(tài)成熟遇到問題一搜一大把學(xué)院信息中心隨便找個會 Java 的老師都能接手維護(hù)。我一般會這么分層controller只做參數(shù)校驗和路由service放業(yè)務(wù)邏輯尤其是積分計算和審核狀態(tài)流轉(zhuǎn)mapper或repository只管數(shù)據(jù)存取entity對應(yīng)數(shù)據(jù)庫表dto/vo負(fù)責(zé)前后端數(shù)據(jù)交換。這樣分的好處是積分規(guī)則以后要改只動 service 層就行不會牽一發(fā)動全身。2.2 目錄結(jié)構(gòu)長什么樣一個能跑起來的項目目錄大概是這樣組織的student-points/ ├── src/main/java/com/example/points/ │ ├── controller/ # 接口層用戶、積分、審核、統(tǒng)計 │ │ ├── UserController.java │ │ ├── PointController.java │ │ ├── AuditController.java │ │ └── StatController.java │ ├── service/ # 業(yè)務(wù)層積分計算、審核流轉(zhuǎn) │ │ ├── PointService.java │ │ ├── AuditService.java │ │ └── impl/ │ ├── mapper/ # 數(shù)據(jù)訪問層 │ ├── entity/ # 數(shù)據(jù)庫實體 │ │ ├── User.java │ │ ├── PointRecord.java │ │ └── PointRule.java │ ├── dto/ # 請求參數(shù)對象 │ ├── vo/ # 返回給前端的數(shù)據(jù) │ └── common/ # 統(tǒng)一返回、異常、工具類 ├── src/main/resources/ │ ├── application.yml # 數(shù)據(jù)庫、端口配置 │ ├── mapper/ # MyBatis XML如果用 │ └── static/ # 前端打包后的靜態(tài)資源 └── pom.xml # Maven 依賴前端如果是獨立工程一般是src/views/points/下放積分錄入、審核列表、統(tǒng)計圖表幾個頁面src/api/里統(tǒng)一封裝 axios 請求。這種結(jié)構(gòu)不新鮮但勝在清晰新人接手看一眼目錄就知道去哪找代碼。2.3 數(shù)據(jù)庫表設(shè)計的關(guān)鍵幾張積分系統(tǒng)的核心就三張表用戶表、積分規(guī)則表、積分記錄表。規(guī)則表決定了「什么行為加多少分」記錄表記錄了「誰在什么時候因為什么加了多少分、當(dāng)前審核狀態(tài)」。-- 積分規(guī)則表把加分項做成可配置的 CREATE TABLE point_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(64) NOT NULL COMMENT 規(guī)則名稱如“學(xué)科競賽一等獎”, category VARCHAR(32) NOT NULL COMMENT 分類學(xué)業(yè)/競賽/實踐/違紀(jì), score DECIMAL(5,1) NOT NULL COMMENT 分值可為負(fù), need_audit TINYINT DEFAULT 1 COMMENT 是否需要審核, enabled TINYINT DEFAULT 1 COMMENT 是否啟用 ); -- 積分記錄表每條加分/扣分都是一條記錄 CREATE TABLE point_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, rule_id BIGINT NOT NULL, score DECIMAL(5,1) NOT NULL, reason VARCHAR(255) COMMENT 具體事由, proof_url VARCHAR(255) COMMENT 證明材料圖片地址, status TINYINT DEFAULT 0 COMMENT 0待審 1通過 2駁回, auditor_id BIGINT, audit_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );把規(guī)則和記錄分開是這套系統(tǒng)最值得抄的一點。很多新手會直接把分值寫死在代碼里結(jié)果學(xué)院說「今年競賽加分標(biāo)準(zhǔn)變了」你就得改代碼重新部署。做成規(guī)則表之后管理員在后臺點幾下就能調(diào)整歷史記錄也不受影響。3. 核心模塊拆解積分計算、審核流轉(zhuǎn)與統(tǒng)計導(dǎo)出3.1 積分計算別在數(shù)據(jù)庫里做累加積分計算看起來簡單就是把某個學(xué)生所有status1的記錄加起來。但這里有個坑如果每次查總分都去SUM(score)數(shù)據(jù)量大了會慢而且統(tǒng)計維度一多按學(xué)期、按分類就頂不住。常見做法是加一張積分匯總表審核通過時同步更新查詢直接讀匯總。// PointService.java 核心方法 public void auditPass(Long recordId, Long auditorId) { PointRecord record pointRecordMapper.selectById(recordId); if (record null || record.getStatus() ! 0) { throw new BizException(記錄不存在或已審核); } // 1. 更新記錄狀態(tài) record.setStatus(1); record.setAuditorId(auditorId); record.setAuditTime(new Date()); pointRecordMapper.updateById(record); // 2. 同步更新匯總表按學(xué)生學(xué)期維度 pointSummaryMapper.addScore( record.getStudentId(), currentTerm(), // 當(dāng)前學(xué)期如 2024-2025-1 record.getScore() ); }這段邏輯的關(guān)鍵在于「審核通過」和「匯總更新」必須在同一個事務(wù)里否則審核過了但總分沒加上學(xué)生來查就是玄學(xué)問題。參數(shù)上currentTerm()一般從配置或系統(tǒng)時間推算學(xué)期劃分規(guī)則各學(xué)院不同建議做成可配置項而不是寫死。3.2 審核流轉(zhuǎn)狀態(tài)機比 if-else 靠譜審核這塊最容易寫成一堆if (status 0) ... else if (status 1)。項目里如果只有「待審-通過-駁回」三態(tài)還好一旦加上「撤回」「批量審核」「申訴」if-else 就會失控。我一般會用一個簡單的狀態(tài)機來管。// 審核狀態(tài)流轉(zhuǎn)用枚舉約束合法遷移 public enum AuditStatus { PENDING(0), PASSED(1), REJECTED(2), APPEALING(3); private final int code; AuditStatus(int code) { this.code code; } // 定義哪些狀態(tài)可以遷移到哪些狀態(tài) public boolean canTransferTo(AuditStatus target) { switch (this) { case PENDING: return target PASSED || target REJECTED; case REJECTED: return target APPEALING; // 駁回后可申訴 case APPEALING:return target PASSED || target REJECTED; default: return false; } } }這樣每次改狀態(tài)前先canTransferTo校驗一下非法流轉(zhuǎn)直接拋異常。好處是以后加狀態(tài)只改枚舉業(yè)務(wù)代碼不用動。參數(shù)說明code存數(shù)據(jù)庫枚舉本身用于邏輯判斷兩者要能互相轉(zhuǎn)換。3.3 統(tǒng)計導(dǎo)出POI 導(dǎo)出別踩內(nèi)存的坑學(xué)院最后要的往往是一張按班級、按分類匯總的 Excel。用 Apache POI 導(dǎo)出時新手容易一次性把幾萬條記錄全查出來塞進(jìn)XSSFWorkbook內(nèi)存直接爆掉。常見做法是分頁查詢 SXSSFWorkbook流式寫出。// 導(dǎo)出積分匯總用 SXSSF 控制內(nèi)存 public void exportSummary(HttpServletResponse response) throws IOException { response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenamepoints.xlsx); // 保留最近 100 行在內(nèi)存其余寫臨時文件 SXSSFWorkbook workbook new SXSSFWorkbook(100); Sheet sheet workbook.createSheet(積分匯總); int page 1, size 500; ListPointSummaryVO list; int rowIdx 0; do { list pointSummaryMapper.pageQuery((page - 1) * size, size); for (PointSummaryVO vo : list) { Row row sheet.createRow(rowIdx); row.createCell(0).setCellValue(vo.getStudentName()); row.createCell(1).setCellValue(vo.getClassName()); row.createCell(2).setCellValue(vo.getTotalScore()); } page; } while (list.size() size); workbook.write(response.getOutputStream()); workbook.dispose(); // 清理臨時文件別漏 }SXSSFWorkbook(100)里的 100 是內(nèi)存中保留的行數(shù)越小越省內(nèi)存但臨時文件 IO 越多一般 100 到 500 之間比較平衡。dispose()一定要調(diào)否則臨時文件會堆在磁盤上這也是個血淚經(jīng)驗。4. 本地跑起來從建庫到登錄的完整步驟4.1 環(huán)境準(zhǔn)備與數(shù)據(jù)庫初始化先把環(huán)境對齊JDK 8 或 11、Maven 3.6、MySQL 5.7 或 8.0、Node 14如果前端獨立。數(shù)據(jù)庫這塊先建庫再執(zhí)行初始化腳本。# 1. 建庫字符集用 utf8mb4別用 utf8 mysql -u root -p -e CREATE DATABASE student_points DEFAULT CHARSET utf8mb4; # 2. 導(dǎo)入表結(jié)構(gòu)和初始數(shù)據(jù) mysql -u root -p student_points sql/schema.sql mysql -u root -p student_points sql/data.sql # 3. 確認(rèn)初始賬號一般在 data.sql 里 # 管理員 admin / 123456學(xué)生賬號學(xué)號登錄utf8mb4這點別省學(xué)生姓名里偶爾有生僻字用utf8會存進(jìn)去變問號到時候排查半天。4.2 配置文件與啟動application.yml里主要改數(shù)據(jù)庫連接和文件上傳路徑server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/student_points?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 你的密碼 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB # 證明材料圖片大小上限 max-request-size: 20MB file: upload-path: /data/points/proof/ # 上傳文件落盤目錄確保有寫權(quán)限serverTimezone必須配不然 MySQL 8 連接會報時區(qū)錯誤這是新手最常見的翻車點之一。upload-path指向的目錄要提前建好并給寫權(quán)限否則上傳證明材料時直接 500。啟動就兩條命令# 后端 mvn clean package -DskipTests java -jar target/student-points-1.0.jar # 前端獨立工程時 npm install npm run dev看到Started Application in x seconds就說明后端起來了瀏覽器訪問http://localhost:8080應(yīng)該能看到登錄頁。4.3 驗證核心流程是否通跑起來之后別急著改代碼先走一遍完整流程確認(rèn)環(huán)境沒問題用管理員登錄在「積分規(guī)則」里加一條規(guī)則比如「志愿服務(wù) 2 分」再用學(xué)生賬號登錄提交一條積分申請切回管理員審核通過最后看統(tǒng)計頁面總分有沒有變。這一圈走通說明數(shù)據(jù)庫、后端、前端、文件上傳這幾條鏈路都是好的。如果哪一步卡住先看后端控制臺日志再看瀏覽器 Network 面板的接口返回基本能定位到是配置問題還是代碼問題。5. 避坑與常見問題排查5.1 積分總分對不上差個幾分現(xiàn)象是學(xué)生查到的總分和輔導(dǎo)員手動算的對不上。原因通常是匯總表更新漏了比如審核通過時事務(wù)回滾了但記錄狀態(tài)已經(jīng)改了或者歷史數(shù)據(jù)是直接改數(shù)據(jù)庫導(dǎo)入的沒走審核流程。解決辦法是寫一個對賬接口定期用SUM(score)重算匯總表并比對不一致的以記錄表為準(zhǔn)修正。我一般會在管理后臺留一個「重算積分」的按鈕出問題點一下。5.2 上傳證明材料報 413 或 500現(xiàn)象是學(xué)生傳競賽證書圖片時失敗。413 是請求體超了max-file-size改 yml 里的限制即可500 多半是upload-path目錄不存在或沒寫權(quán)限。解決先mkdir -p建目錄再chmod給權(quán)限然后確認(rèn)配置里的路徑是絕對路徑。別用相對路徑打成 jar 之后工作目錄會變相對路徑就找不到了。5.3 審核列表分頁越翻越慢現(xiàn)象是積分記錄多了以后審核列表翻到后面幾頁明顯卡。原因是用了LIMIT offset, size且 offset 很大MySQL 要掃描前面所有行。解決辦法是用游標(biāo)分頁記住上一頁最后一條的 id查詢時WHERE id lastId LIMIT size。參數(shù)上lastId由前端每次請求帶上后端返回時也帶上當(dāng)前頁最后一條 id。5.4 并發(fā)審核同一條記錄現(xiàn)象是兩個管理員同時點通過結(jié)果匯總表加了兩次分。原因是查詢和更新之間沒有鎖。解決辦法是在更新記錄狀態(tài)時加樂觀鎖UPDATE point_record SET status1 WHERE id? AND status0根據(jù)影響行數(shù)判斷是否搶到搶不到就提示「該記錄已被處理」。這個坑不常遇到但一旦遇到就是數(shù)據(jù)錯亂值得提前防。5.5 學(xué)期切換后歷史數(shù)據(jù)串了現(xiàn)象是新學(xué)期開始后學(xué)生看到的總分把上學(xué)期也算進(jìn)去了。原因是匯總表沒按學(xué)期隔離或者查詢時沒帶學(xué)期條件。解決匯總表加term字段查詢默認(rèn)只查當(dāng)前學(xué)期需要看歷史時顯式傳學(xué)期參數(shù)。學(xué)期劃分規(guī)則做成配置別寫死在代碼里。6. 二次開發(fā)把積分規(guī)則做成可配置引擎拿到這套源碼最值得動手改的地方是積分規(guī)則引擎。現(xiàn)在規(guī)則表里只有「名稱、分類、分值」屬于靜態(tài)配置。但真實場景里加分往往有條件比如「競賽一等獎加 5 分但同一競賽每學(xué)期最多加一次」「志愿服務(wù)每小時 0.5 分單學(xué)期上限 10 分」。這些用一張靜態(tài)表表達(dá)不了得引入規(guī)則表達(dá)式。我的做法是在point_rule表加一個condition_expr字段存一段可解析的表達(dá)式比如count(ruleId3, termcurrent) 1表示該規(guī)則本學(xué)期最多觸發(fā)一次sum(category實踐, termcurrent) score 10表示實踐類總分封頂。然后用一個輕量的表達(dá)式引擎比如 Aviator 或 SpEL在提交積分時校驗。// 提交積分前做規(guī)則校驗 public void submitPoint(Long studentId, Long ruleId, String reason) { PointRule rule ruleMapper.selectById(ruleId); if (rule.getConditionExpr() ! null) { // 構(gòu)造上下文該學(xué)生本學(xué)期已有記錄 MapString, Object ctx new HashMap(); ctx.put(count, buildCountFunction(studentId)); ctx.put(sum, buildSumFunction(studentId)); ctx.put(score, rule.getScore()); Boolean pass (Boolean) AviatorEvaluator.execute(rule.getConditionExpr(), ctx); if (Boolean.FALSE.equals(pass)) { throw new BizException(不滿足加分條件 rule.getRuleName()); } } // 落庫為待審記錄 // ... }這樣改的好處是以后學(xué)院調(diào)整加分政策管理員在后臺改表達(dá)式就行不用找你改代碼。代價是要給管理員做個表達(dá)式編輯界面并且做好校驗防止寫錯表達(dá)式導(dǎo)致提交全掛。我一般會提供一個「試算」功能讓管理員填幾個模擬數(shù)據(jù)看看表達(dá)式結(jié)果對不對再保存。驗證改造是否成功別只看單條提交要構(gòu)造邊界用例同一競賽提交兩次看第二次是否被攔、實踐類加到上限看是否還能加、表達(dá)式寫錯時是否給出友好提示而不是 500。這幾條過了規(guī)則引擎才算穩(wěn)。從那以后我每次給這類系統(tǒng)加「可配置」功能都強制先寫一組邊界用例跑一遍再交給業(yè)務(wù)方試用。規(guī)則引擎這種東西配置錯了比代碼錯了更難查因為代碼有編譯期檢查表達(dá)式只有運行時才知道。希望這套思路能幫到你拿到源碼后先跑通主流程再按自己學(xué)院的加分細(xì)則去改規(guī)則表基本就能直接用了。本文還有配套的精品資源點擊獲取