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