:數(shù)據(jù)模型與狀態(tài)機設計實戰(zhàn))
簡介基于微信小程序的新生報到系統(tǒng)完整源碼包附帶系統(tǒng)分析、設計、測試等說明文檔適合畢業(yè)設計、課程設計或小程序開發(fā)實戰(zhàn)練習使用。系統(tǒng)覆蓋新生報到常見環(huán)節(jié)包含小程序端、后臺管理端等模塊采用前后端分離架構可直接部署運行或作為二次開發(fā)模板。壓縮包內共1548個文件大小21.73MB其中png圖片用于界面素材js/wxml/wxss構成小程序邏輯與頁面vue/java搭建管理后臺與后端服務json存放配置信息docx提供詳細文檔說明文檔章節(jié)涵蓋可行性分析、性能需求、數(shù)據(jù)庫設計及系統(tǒng)測試能幫助理解完整項目流程。已有528人學習瀏覽具有一定參考價值資源完整度高既有可運行代碼又有數(shù)據(jù)庫E/R圖、功能結構等文檔尤其適合需要快速搭建類似系統(tǒng)或撰寫相關論文的人群。1. 基于微信小程序的新生報到系統(tǒng)不是做一個“報名頁”標題里的“新生報到”和“報名”是兩碼事。報名解決的是“有沒有這個人”報到解決的是“這個人的入學手續(xù)走完沒有”。基于微信小程序的新生報到系統(tǒng)要承接錄取數(shù)據(jù)核驗、信息補錄、費用繳納、宿舍分配、軍訓服裝尺碼登記這些跨部門動作最后產生一張可查驗的報到單。一個只做前端表單的演示項目撐不起這個標題沒有狀態(tài)機設計的源碼后面改起來也會一地雞毛。新生報到的特點是時間短、數(shù)據(jù)集中、部門交錯。招生辦有錄取名單財務處管繳費宿管中心管床位輔導員管到校確認小程序只是把這些數(shù)據(jù)實時匯總的窗口。源碼加說明文檔的項目面向的是需要二次開發(fā)的團隊不是拿來就能跑的 SaaS。讀這套源碼前先分清哪些邏輯綁定微信生態(tài)登錄、消息提醒、手機號授權哪些是報到業(yè)務本身錄取核驗、繳費、宿舍分配。下面先從數(shù)據(jù)模型開始因為這里最容易暴露源碼的坑。2. 新生報到系統(tǒng)的技術選型和數(shù)據(jù)模型設計2.1 微信小程序作為報到端的技術邊界一個新生報到系統(tǒng)通常包含三個端新生用的小程序端、輔導員和管理員用的電腦端、以及服務器端。標題既然明確“基于微信小程序”前端基本鎖死但后端可以是 Java Spring Boot、Node.js、PHP也可以是小程序云開發(fā)。判斷源碼可維護性的第一件事是看它有沒有把微信登錄的appsecret放在前端。如果在前端直接請求微信接口這套源碼可以直接放棄。自建后端和小程序云開發(fā)是兩條路線。自建后端適合對接學校已有的統(tǒng)一身份認證、財務系統(tǒng)、宿管系統(tǒng)云開發(fā)適合快速上線、周期短、不依賴學校內部網(wǎng)絡的項目。新生報到系統(tǒng)如果只是畢業(yè)設計云開發(fā)能省去服務器和域名備案的麻煩如果要真實部署學校現(xiàn)有的財務、學工數(shù)據(jù)大概率要走 HTTP 接口或數(shù)據(jù)庫中間庫自建后端更穩(wěn)妥。拿到源碼后先看它是wx.cloud還是wx.request這決定了后續(xù)改造的工作量。2.2 接口、管理端和數(shù)據(jù)庫的職責拆分常見做法是后端統(tǒng)一提供 REST API管理端和小程序端共用同一套接口差別只體現(xiàn)在登錄角色和權限上。業(yè)務邏輯全寫在小程序端是很多“偽源碼”的通病前端直接讀寫云數(shù)據(jù)庫或者把前端表單數(shù)據(jù)直接發(fā)到第三方接口導致同一個學號重復報到、學院統(tǒng)計對不上賬這類問題。接口層面最核心的是身份綁定關系。微信登錄拿到的是openid這個值只跟微信號相關跟學號、身份證號沒有任何關系。報到系統(tǒng)的標準綁定流程是小程序端先wx.login拿code換openid再讓新生填一次錄取編號和身份證號后六位后端校驗通過后把openid寫到該學生的記錄上。以后每次請求后端通過 token 里的 userId 找到對應學生不再需要重復驗證學號。2.3 報到業(yè)務核心表設計與狀態(tài)機報到流程可以抽象成四個主狀態(tài)待報到、信息已確認、財務已辦理、報到完成。實際迎新系統(tǒng)里還會有宿舍分配、綠色通道、軍訓服裝、校園卡等子業(yè)務但不要為每個子業(yè)務單獨造幾十張表。比較穩(wěn)妥的設計是學生主表只存基礎信息和整體報到狀態(tài)子步驟統(tǒng)一放在“報到進度表”里用step_code區(qū)分步驟。CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, admission_no VARCHAR(20) NOT NULL COMMENT 錄取編號, name VARCHAR(50) NOT NULL COMMENT 姓名, id_card VARCHAR(18) NOT NULL COMMENT 身份證號, college_id INT NOT NULL COMMENT 二級學院ID, major_id INT NOT NULL COMMENT 專業(yè)ID, enroll_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未報到 1已報到, openid VARCHAR(64) NOT NULL DEFAULT COMMENT 微信openid, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT 新生基礎信息表; CREATE TABLE report_step ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 學生ID關聯(lián)student.id, step_code VARCHAR(30) NOT NULL COMMENT info|fee|dorm|final, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未完成 1已完成, finish_time DATETIME NULL COMMENT 步驟完成時間 ) COMMENT 報到進度表;用report_step而不是在學生表里放一堆fee_status、dorm_status字段是因為報到步驟在不同學校可能是動態(tài)配置的。今年可能多一個“體檢預約”明年可能取消“軍訓服登記”用獨立行存步驟能直接加配置不必改表結構。配套的step_config表用來定義步驟順序、是否啟用、前置條件字段類型說明step_codevarchar(30)步驟標識如 feestep_namevarchar(50)步驟顯示名如 財務繳費sort_orderint排序值小的先執(zhí)行enabledtinyint是否啟用pre_conditionsvarchar(255)前置步驟碼逗號分隔狀態(tài)機的核心規(guī)則是后端在更新步驟狀態(tài)時必須校驗前置步驟已完成。例如宿舍分配dorm步驟如果info還沒完成就不能置為已完成。這個校驗不能只在小程序端做否則繞過小程序直接調接口就能亂改數(shù)據(jù)。狀態(tài)流轉建議單獨放在一個服務類里統(tǒng)一處理。public boolean completeStep(Long studentId, String stepCode) { ListReportStep steps reportStepMapper.selectByStudent(studentId); MapString, ReportStep map steps.stream() .collect(Collectors.toMap(ReportStep::getStepCode, Function.identity())); StepConfig config stepConfigMapper.selectByCode(stepCode); if (!config.getEnabled()) { throw new RuntimeException(該步驟未啟用); } if (map.get(stepCode).getStatus() ! 1) { for (String pre : config.getPreConditions().split(,)) { if (map.get(pre.trim()).getStatus() ! 1) { throw new RuntimeException(前置步驟未完成); } } map.get(stepCode).setStatus(1); map.get(stepCode).setFinishTime(new Date()); reportStepMapper.update(map.get(stepCode)); } return true; }pre_conditions由配置表提供避免把步驟依賴寫死在業(yè)務方法里。參數(shù)說明studentId是學生主鍵stepCode是步驟碼preConditions是一個逗號分隔的字符串配置時按順序寫入。真實源碼里最容易被省略的就是這段校驗結果就是統(tǒng)計報表出現(xiàn)“未繳費卻已分宿舍”的臟數(shù)據(jù)。讀源碼時優(yōu)先搜update report_step如果沒有任何前置判斷說明這個源碼還需要自己補狀態(tài)機。3. 用小程序端跑通新生報到主流程登錄、綁定、報到單3.1 微信登錄換 openid不直接把學號當賬號這一步是微信小程序開發(fā)里最常見的分水嶺。很多項目還在用wx.getUserProfile()拿昵稱頭像當?shù)卿洃{據(jù)這在現(xiàn)在的微信生態(tài)里已經(jīng)不可靠用戶點拒絕就進不來。更不推薦把學號和身份證直接放在本地 storage 里當?shù)卿洃B(tài)。正確的做法是靜默登錄加業(yè)務綁定兩步走。前端先執(zhí)行wx.login拿到一次性的codeonLoad() { wx.login({ success: (res) { wx.request({ url: ${this.data.baseUrl}/api/wx/login, method: POST, data: { code: res.code }, success: (response) { this.setData({ token: response.data.token }); wx.setStorageSync(token, response.data.token); this.checkBinding(); } }); }, fail: (err) console.error(wx.login 失敗, err) }); }code是臨時憑證5分鐘內有效后端拿它去微信的code2session接口換openid和session_key。前端不要碰session_key也不要自己解析 JWT直接讓后端返回業(yè)務系統(tǒng)自己的 token 即可。如果源碼是uni-app工程那么在 HBuilderX 里運行后仍然會產出微信小程序包只是調試時需要在 HBuilderX 中先配置微信開發(fā)者工具的安裝路徑否則運行時找不到模擬器。后端接口示意PostMapping(/api/wx/login) public LoginResponse login(RequestBody WxLoginRequest request) { String url String.format( https://api.weixin.qq.com/sns/jscode2session?appid%ssecret%sjs_code%sgrant_typeauthorization_code, wxConfig.getAppid(), wxConfig.getSecret(), request.getCode()); String resp restTemplate.getForObject(url, String.class); JsonNode node objectMapper.readTree(resp); String openid node.get(openid).asText(); String token jwtUtil.createToken(openid); return LoginResponse.builder().token(token).openid(openid).build(); }參數(shù)說明appid和secret從小程序后臺的“開發(fā)管理-開發(fā)設置-開發(fā)者ID”里拿。code必須一次性使用重復使用會報invalid code。如果返回結果里沒有openid而是errcode 40029說明code已過期或者被代理工具重復提交過。3.2 用“錄取編號 身份證后六位”完成學籍綁定拿到openid后小程序要檢查當前微信號是否已經(jīng)綁定了新生學籍。未綁定時頁面跳轉到綁定頁輸入錄取編號和身份證后六位bindStudent() { wx.request({ url: ${this.data.baseUrl}/api/wx/bind, method: POST, data: { admissionNo: this.data.admissionNo.trim(), idCardSuffix: this.data.idCardSuffix.trim(), token: wx.getStorageSync(token) }, success: (res) { if (res.data.code 0) { wx.showToast({ title: 綁定成功 }); wx.reLaunch({ url: /pages/index/index }); } else { wx.showModal({ title: 綁定失敗, content: res.data.msg, showCancel: false }); } } }); }后端處理綁定請求時不能只校驗錄取編號是否存在還要檢查該學生記錄的openid字段是否已被占用。如果非空說明這個學號已經(jīng)被其他微信綁定過應當提示“請聯(lián)系輔導員解綁”。新生報到場景里經(jīng)常出現(xiàn)家長先掃碼綁定孩子到校后又用自己的微信綁一次這個防重復綁定邏輯是必須的。3.3 報到進度頁與報到單生成綁定成功后小程序主頁應該是“報到進度”列表數(shù)據(jù)來源直接就是report_step表。前端不要寫死步驟數(shù)組應按照后端返回的 steps 數(shù)組渲染。這樣后臺在step_config里調順序、開關步驟小程序端不用發(fā)新版本。getProgress() { wx.request({ url: ${this.data.baseUrl}/api/report/progress, method: GET, header: { Authorization: Bearer ${wx.getStorageSync(token)} }, success: (res) { const steps res.data.steps.map((s) ({ code: s.stepCode, name: s.stepName, status: s.status })); this.setData({ steps, finish: res.data.finish }); } }); }報到單生成一般有兩種實現(xiàn)一種是后端用 PDF 模板生成文件返回下載鏈接另一種是直接在小程序里用canvas畫一張圖片方便新生保存在相冊里。如果源碼是 canvas 方案要注意高分屏下的模糊問題。解決辦法是使用wx.getSystemInfoSync().pixelRatio把 canvas 寬高乘上pixelRatio再通過 CSS 縮放回邏輯像素。3.4 信息采集表單、單選框和圖片上傳的正確寫法新生報到里最常采集的是性別、民族、政治面貌、是否申請綠色通道、軍訓服裝尺碼。這些字段不要全用input能單選就radio-group可選就picker。尤其是“是否綠色通道”這種影響財務審核的字段前后端的枚舉值必須一致否則統(tǒng)計報表會出現(xiàn)“是/否/1/0”混在一起的情況。radio-group bindchangeonGreenChannelChange label radio value0 checked{{form.greenChannel 0}} / 否 /label label radio value1 checked{{form.greenChannel 1}} / 是 /label /radio-group上傳錄取通知書或證件照時推薦用wx.chooseMedia替代已經(jīng)廢棄的wx.chooseImagewx.chooseMedia({ count: 1, mediaType: [image], sourceType: [album, camera], success: (res) { const filePath res.tempFiles[0].tempFilePath; wx.uploadFile({ url: ${this.data.baseUrl}/api/upload, filePath, name: file, formData: { type: notice }, success: (r) { const data JSON.parse(r.data); wx.showToast({ title: 上傳成功 }); } }); } });name字段是后端接收文件的表單字段名必須和后端MultipartFile的參數(shù)名一致formData里附帶業(yè)務類型比如notice代表錄取通知書face代表證件照。注意wx.uploadFile的返回值r.data是字符串不能直接當 JSON 對象用必須先JSON.parse。4. 管理端后臺新生數(shù)據(jù)導入、報到進度統(tǒng)計、導出報表4.1 管理端權限控制角色與數(shù)據(jù)范圍管理端不能和新生端共用同一個登錄頁。常見做法是后臺管理頁用賬號密碼登錄登錄后簽發(fā)一個包含role的 token接口層通過攔截器校驗角色。新生端 token 的role是student管理端是admin或operator。報到的寫操作必須以學生 token 身份執(zhí)行管理端主要做導入、查詢、統(tǒng)計、導出避免誤操作覆蓋學生自己提交的信息。后端攔截器示例public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization).replaceFirst(Bearer , ); Claims claims jwtUtil.parseToken(token); if (student.equals(claims.get(role))) { return request.getRequestURI().startsWith(/api/report); } return request.getRequestURI().startsWith(/api/admin); }這段代碼只做演示生產環(huán)境建議直接用 Spring Security 或 Shiro。讀源碼時檢查管理端接口是否都掛在/api/admin前綴下如果管理接口和學生接口混在同一個 Controller 里權限就得靠方法級注解控制維護成本會高很多。4.2 批量導入新生名單替代手工錄入招辦給過來的數(shù)據(jù)通常是 Excel直接導入系統(tǒng)最省事的是先轉成 CSV。用 Spring Boot 讀取 CSV 的簡化版實現(xiàn)如下PostMapping(/api/admin/student/import) public ImportResult importStudents(RequestParam(file) MultipartFile file) { ListStudentImportDto list new ArrayList(); try (BufferedReader reader new BufferedReader( new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { String line; reader.readLine(); // 跳過頭行 while ((line reader.readLine()) ! null) { String[] f line.split(,); if (f.length 4) { continue; } StudentImportDto dto new StudentImportDto(); dto.setAdmissionNo(f[0].trim()); dto.setName(f[1].trim()); dto.setIdCard(f[2].trim()); dto.setCollegeId(Integer.parseInt(f[3].trim())); list.add(dto); } } catch (IOException e) { throw new RuntimeException(文件讀取失敗); } return studentService.batchInsert(list); }這里要注意 CSV 編碼問題。Excel 默認導出的 CSV 可能是 GBK讀取時如果固定用 UTF-8 會出現(xiàn)中文亂碼。最不容易出錯的做法是在導入頁上提示用戶“另存為 CSV UTF-8 格式”同時后端在讀取前用 BOM 或字符集探測做一次判斷。源碼里如果只支持 UTF-8需要自己補上這個容錯。4.3 報到進度實時統(tǒng)計與導出報到系統(tǒng)管理者最關心的是“整體報到率多少哪個學院落后了”。一條 SQL 就能解決SELECT s.college_id, COUNT(s.id) AS total_students, SUM(CASE WHEN s.enroll_status 1 THEN 1 ELSE 0 END) AS reported_count FROM student s GROUP BY s.college_id;如果要鉆取到專業(yè)和班級把college_id替換成major_id或class_name即可。如果還需要按步驟查看完成情況則查report_step表SELECT rs.step_code, COUNT(DISTINCT rs.student_id) AS finished_count FROM report_step rs WHERE rs.status 1 GROUP BY rs.step_code;導出功能不要放在小程序端。小程序的文件下載能力很弱管理端直接提供接口后端生成 CSV 并返回文件流GetMapping(/api/admin/report/export) public void export(HttpServletResponse response) throws IOException { response.setContentType(text/csv; charsetUTF-8); response.setHeader(Content-Disposition, attachment; filenamereport_ System.currentTimeMillis() .csv); PrintWriter writer response.getWriter(); writer.write(學號,姓名,學院,報到狀態(tài)\n); // 按查詢結果循環(huán) writer.write(...) writer.flush(); }Content-Disposition的filename建議用時間戳避免瀏覽器緩存同名文件。charset用 UTF-8 時Excel 打開 CSV 會有中文亂碼風險需在輸出內容最前面加 BOM 頭\uFEFF。這個小細節(jié)在實際迎新導出報表時經(jīng)常被忽略。4.4 說明文檔的檢查清單“源碼 說明文檔”的項目重點要檢查文檔里有沒有這四樣東西數(shù)據(jù)庫初始化腳本、前端 appid 配置位、后端配置文件樣例、核心接口的請求響應示例。如果說明文檔只有一句“導入數(shù)據(jù)庫填上 appid”那只能算 README不能算說明文檔。檢查項如下文檔項內容要求數(shù)據(jù)庫初始化建表 SQL、初始管理員賬號、狀態(tài)碼字典后端啟動JDK/Node 版本、配置文件模板、端口、數(shù)據(jù)庫地址小程序配置appid、request 合法域名、業(yè)務域名、上傳校驗域名接口文檔每個接口的 URL、請求參數(shù)、返回碼、錯誤示例拿到源碼后按這個順序過一遍能避免“跑不起來”的尷尬。很多項目代碼寫得不差就卡在文檔缺少配置說明。5. 從源碼到可演示的新生報到小程序部署順序和自定義導航欄高度適配先給部署順序建庫執(zhí)行 SQL、改后端配置、啟動后端、導入源碼到微信開發(fā)者工具、設置 appid、開發(fā)環(huán)境勾選“不校驗合法域名”。注意“不校驗合法域名”只能用于本地開發(fā)真機預覽或體驗版必須配置 request 合法域名否則接口全部請求失敗。這一步卡住過很多第一次跑微信小程序源碼的人。經(jīng)常有人問“怎么修改剛進入的加載頁面”。很多報到源碼的入口頁是pages/login/login部署時想直接進入報到進度頁只需要修改app.json中的 pages 數(shù)組{ pages: [ pages/report/report, pages/login/login, pages/profile/profile ] }pages數(shù)組第一項就是啟動頁。改完后要注意wx.reLaunch和wx.navigateBack是否有對應路由否則會白屏。最后重點說自定義頂部導航欄高度適配。默認導航欄在 iPhone 和 Android 上高度不一樣如果源碼里用的是自定義導航欄必須動態(tài)計算。標準做法是通過菜單按鈕位置反推const { statusBarHeight } wx.getSystemInfoSync(); const menuButton wx.getMenuButtonBoundingClientRect(); const navBottom menuButton.bottom; const navBarHeight navBottom (menuButton.top - statusBarHeight) * 2;statusBarHeight是狀態(tài)欄高度劉海屏和非劉海屏數(shù)值不同menuButton.bottom是膠囊按鈕底部到屏幕頂部的距離。自定義導航欄的總高度要用navBottom 上下間距*2而不是menuButton.top。這段話可以封裝到utils/navigation.js里所有自定義導航欄頁面在onLoad時調用一次。驗證方法分別用 iPhone 13 和一款 Android 手機跑真機預覽看頁面標題是否垂直居中膠囊按鈕有沒有被自定義導航欄遮擋。如果偏上就是navBarHeight少加了(menuButton.top - statusBarHeight) * 2這一項。確認無誤后再去處理接口調試和抓包觀察抓包排查時要保證測試手機已安裝對應證書并且只用于開發(fā)環(huán)境驗證。上線前記得取消開發(fā)者工具里的“不校驗合法域名”避免正式環(huán)境出現(xiàn)請求被攔截的假故障。本文還有配套的精品資源點擊獲取