級(jí)疫苗預(yù)約系統(tǒng)落地實(shí)踐:SpringBoot+Vue+MyBatis+MySQL)
企業(yè)級(jí)疫苗發(fā)布與接種預(yù)約系統(tǒng)的落地實(shí)踐做這個(gè)項(xiàng)目之前我被疫苗預(yù)約這個(gè)場景折磨過一陣子。社區(qū)衛(wèi)生院的朋友跟我吐槽每到流感季或者新疫苗到貨電話被打爆、現(xiàn)場排長隊(duì)、庫存對不上光靠Excel登記根本撐不住。聊完我就明白這玩意兒不是簡單做個(gè)能預(yù)約的小程序就完事的它牽涉疫苗批次發(fā)布、庫存分配、預(yù)約時(shí)段管理、接種記錄追溯、多機(jī)構(gòu)協(xié)作是一個(gè)典型的企業(yè)級(jí)信息管理系統(tǒng)。用SpringBootVueMyBatisMySQL這套組合去落地老實(shí)說是性價(jià)比最高的選擇——團(tuán)隊(duì)好招、社區(qū)資料多、部署不挑環(huán)境而且這套架構(gòu)對一個(gè)管理型系統(tǒng)來說完全夠用。這篇博客我會(huì)按我做這個(gè)項(xiàng)目的實(shí)際流程來寫從業(yè)務(wù)梳理、表結(jié)構(gòu)設(shè)計(jì)、后端核心邏輯、前端工程化到部署踩坑都過一遍。如果你正準(zhǔn)備做類似的管理系統(tǒng)或者想拿一套完整的疫苗預(yù)約項(xiàng)目做學(xué)習(xí)參考這篇內(nèi)容應(yīng)該能幫你少走不少彎路。1. 疫苗預(yù)約場景下的業(yè)務(wù)痛點(diǎn)和系統(tǒng)定位1.1 這個(gè)系統(tǒng)到底要解決什么問題疫苗預(yù)約系統(tǒng)跟普通電商預(yù)約不一樣它有幾個(gè)非常特殊的業(yè)務(wù)約束。第一個(gè)是庫存與批次強(qiáng)綁定。疫苗不是普通商品每一批疫苗都有批號(hào)、生產(chǎn)企業(yè)、有效期、冷鏈記錄用戶預(yù)約的不是一盒疫苗而是某個(gè)批次下的一支疫苗。這個(gè)特性導(dǎo)致庫存表不能只存總數(shù)必須按批次維度管理還要能追溯某個(gè)用戶接種的是哪一批。第二個(gè)是時(shí)段承載量有限。接種門診每天的接待能力是固定的比如上午8:30-11:30、下午14:00-17:00每個(gè)時(shí)段能打多少針取決于醫(yī)生數(shù)量和接種臺(tái)數(shù)量。這意味著預(yù)約系統(tǒng)必須做時(shí)段庫存控制而不是簡單地看總庫存。第三個(gè)是爽約和取消的庫存釋放。很多人預(yù)約了不來如果系統(tǒng)不處理超時(shí)釋放庫存會(huì)被無效占用。我在設(shè)計(jì)時(shí)專門做了預(yù)約狀態(tài)超時(shí)自動(dòng)取消的定時(shí)任務(wù)保證被占用的庫存能回流。第四個(gè)是多機(jī)構(gòu)多角色協(xié)作。疾控中心發(fā)布疫苗接種門診管理預(yù)約用戶選擇門診和時(shí)間管理員做數(shù)據(jù)統(tǒng)計(jì)。所以系統(tǒng)天然要有RBAC權(quán)限模型至少分管理員、門診操作員、普通用戶三個(gè)角色。1.2 系統(tǒng)核心業(yè)務(wù)邊界劃分我把整個(gè)系統(tǒng)拆成四個(gè)核心業(yè)務(wù)域疫苗管理域疫苗品類維護(hù)、批次登記、庫存入庫和出庫記錄。預(yù)約發(fā)布域創(chuàng)建預(yù)約活動(dòng)、配置可預(yù)約批次、設(shè)置時(shí)段容量、發(fā)布公告。預(yù)約交易域用戶選擇疫苗和時(shí)段下單、支付/免費(fèi)確認(rèn)、取消、爽約處理、接種核銷。統(tǒng)計(jì)報(bào)表域按疫苗、機(jī)構(gòu)、時(shí)間段統(tǒng)計(jì)預(yù)約量和接種量生成接種率報(bào)表。一個(gè)合格的系統(tǒng)設(shè)計(jì)最重要的是一開始就把邊界劃清楚。做這個(gè)項(xiàng)目的時(shí)候我最開始犯過的錯(cuò)就是把預(yù)約單和接種記錄混在一張表里后來發(fā)現(xiàn)狀態(tài)流轉(zhuǎn)完全理不清才拆成獨(dú)立的預(yù)約主表和接種記錄表。這個(gè)教訓(xùn)后面講表結(jié)構(gòu)的時(shí)候會(huì)細(xì)說。提示如果你也想做類似系統(tǒng)千萬別一上來就寫代碼。先把業(yè)務(wù)域拆開哪怕是用紙畫一下也比直接建表靠譜得多。2. 技術(shù)選型的取舍邏輯SpringBootVueMyBatisMySQL為什么夠用2.1 不選微服務(wù)不選NoSQL甚至不引入Redis你可能聽說過很多企業(yè)級(jí)項(xiàng)目一上來就上Spring Cloud、引入Redis緩存、用Elasticsearch做搜索。但這個(gè)項(xiàng)目的真實(shí)場景里用戶量級(jí)是一個(gè)城市日活幾千到幾萬的預(yù)約請求而不是雙十一的百萬并發(fā)。對我來說技術(shù)選型最重要的是匹配業(yè)務(wù)規(guī)模和團(tuán)隊(duì)維護(hù)成本其他都是偽需求。我最終敲定SpringBoot 2.7 Vue 2.7 MyBatis MySQL 5.7的組合理由很簡單技術(shù)組件選型理由實(shí)際效果SpringBoot簡化Spring配置內(nèi)置Tomcat快速構(gòu)建REST API開發(fā)效率高打包成jar就能跑Vue 2.7組件化開發(fā)生態(tài)成熟Element UI配套完善后臺(tái)管理界面開發(fā)快MyBatisSQL可控性強(qiáng)預(yù)編譯防注入適合復(fù)雜業(yè)務(wù)查詢預(yù)約報(bào)表SQL可以直接調(diào)優(yōu)MySQL事務(wù)支持可靠InnoDB行級(jí)鎖滿足預(yù)約并發(fā)控制數(shù)據(jù)一致性有保障2.2 MyBatis在預(yù)約場景的優(yōu)勢很多團(tuán)隊(duì)現(xiàn)在喜歡用MyBatis-Plus但我在這個(gè)項(xiàng)目里選擇了原生MyBatis。原因很簡單預(yù)約系統(tǒng)的查詢有大量多表關(guān)聯(lián)和條件組合比如查詢某機(jī)構(gòu)某時(shí)間段內(nèi)某疫苗的預(yù)約明細(xì)這種SQL用XML寫出來語義非常清晰調(diào)整條件也方便。而且MyBatis的一級(jí)緩存和二級(jí)緩存機(jī)制在報(bào)表類查詢上能發(fā)揮很大作用。當(dāng)然純手寫實(shí)體類映射確實(shí)多花了一點(diǎn)時(shí)間但穩(wěn)定性是值得的。注意如果你習(xí)慣用MyBatis-Plus直接把單表CRUD交給它復(fù)雜報(bào)表查詢用原生SQL這樣是效率最優(yōu)方案。不要因?yàn)槭窃鶰yBatis就排斥所有便捷工具架構(gòu)設(shè)計(jì)講究的是各取所長。2.3 前后端分離的協(xié)作模式前端Vue項(xiàng)目通過Vite構(gòu)建開發(fā)時(shí)用proxy代理解決跨域生產(chǎn)環(huán)境打包成dist目錄扔給Nginx托管。后端只暴露REST API統(tǒng)一返回{code, message, data}結(jié)構(gòu)。這個(gè)模式不用我多說是當(dāng)前主流做法。但我要提醒一點(diǎn)接口文檔一定要先定好。我在這項(xiàng)目里用YApi管理接口定義前端后端各自按文檔開發(fā)聯(lián)調(diào)階段幾乎沒有因?yàn)樽侄蚊灰恢路倒ぁ?. 數(shù)據(jù)庫建模與關(guān)鍵表設(shè)計(jì)3.1 用戶與疫苗主數(shù)據(jù)數(shù)據(jù)庫設(shè)計(jì)是這個(gè)系統(tǒng)最核心的部分。我實(shí)際建了12張表這里挑最關(guān)鍵的幾張講。用戶表包含賬號(hào)、密碼BCrypt加密、姓名、身份證號(hào)、手機(jī)號(hào)、角色類型、所屬機(jī)構(gòu)ID。設(shè)計(jì)上注意一個(gè)細(xì)節(jié)身份證號(hào)用于疫苗接種的身份核驗(yàn)所以必須加密存儲(chǔ)不是明文存。疫苗品類表存儲(chǔ)疫苗名稱、適用人群、接種劑次如新冠疫苗需要2針或3針、生產(chǎn)廠家、規(guī)格。這個(gè)表屬于基礎(chǔ)主數(shù)據(jù)跟具體批次無關(guān)。疫苗批次表這是關(guān)鍵表。每一批疫苗到貨后管理員登記批號(hào)、生產(chǎn)企業(yè)、有效期、到貨數(shù)量、冷鏈溫度記錄。一個(gè)批次可以對應(yīng)多個(gè)接種機(jī)構(gòu)也可以分配給一個(gè)機(jī)構(gòu)后單獨(dú)管理。這個(gè)表的設(shè)計(jì)直接決定庫存追溯的能力。CREATE TABLE vaccine_batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, batch_no VARCHAR(50) NOT NULL COMMENT 疫苗批號(hào), vaccine_id BIGINT NOT NULL COMMENT 疫苗品類ID, manufacturer VARCHAR(100) COMMENT 生產(chǎn)企業(yè), production_date DATE COMMENT 生產(chǎn)日期, expire_date DATE COMMENT 有效期至, total_count INT COMMENT 入庫總數(shù)量, available_count INT COMMENT 剩余可用數(shù)量, org_id BIGINT COMMENT 所屬接種機(jī)構(gòu)ID, status TINYINT COMMENT 1正常 2售罄 3過期, create_time DATETIME );3.2 預(yù)約流程的狀態(tài)機(jī)設(shè)計(jì)預(yù)約單表是整個(gè)系統(tǒng)的交易核心。我在設(shè)計(jì)這張表的時(shí)候把預(yù)約單狀態(tài)設(shè)計(jì)成一系列有序的流轉(zhuǎn)狀態(tài)。狀態(tài)0待確認(rèn)用戶提交預(yù)約鎖定庫存狀態(tài)1已確認(rèn)管理員/系統(tǒng)確認(rèn)預(yù)約成功狀態(tài)2已接種用戶到現(xiàn)場完成接種狀態(tài)3已取消用戶主動(dòng)取消狀態(tài)4已過期預(yù)約當(dāng)天結(jié)束未接種自動(dòng)過期預(yù)約單主表主要字段預(yù)約單號(hào)、用戶ID、疫苗批次ID、機(jī)構(gòu)ID、預(yù)約日期、預(yù)約時(shí)段、狀態(tài)、創(chuàng)建時(shí)間、取消時(shí)間、接種時(shí)間、核銷人ID。為了完整性我還設(shè)計(jì)了明細(xì)子表記錄接種的疫苗批號(hào)、接種部位、接種人員、留觀時(shí)間等信息。這樣主表只管狀態(tài)流轉(zhuǎn)詳細(xì)醫(yī)療信息放在子表職責(zé)分明。提示這里強(qiáng)烈建議給預(yù)約單號(hào)建唯一索引。預(yù)約單號(hào)我用時(shí)間戳機(jī)構(gòu)ID隨機(jī)數(shù)生成一方面業(yè)務(wù)需要用戶報(bào)手機(jī)號(hào)單號(hào)核銷另一方面也方便分庫分表雖然目前用不到但預(yù)留了擴(kuò)展空間。3.3 庫存扣減與并發(fā)控制疫苗預(yù)約一定會(huì)遇到并發(fā)問題同一時(shí)間很多人搶同一時(shí)段同一批次的疫苗。如果代碼先查庫存再update妥妥的超賣。我的處理方式是用MySQL的樂觀鎖條件更新在MyBatis的update語句里加庫存判斷只有影響行數(shù)為1才算扣減成功。UPDATE vaccine_batch SET available_count available_count - 1 WHERE id #{batchId} AND available_count 0這條SQL的精髓在于把檢查庫存和扣減庫存合并成一個(gè)原子操作數(shù)據(jù)庫的行鎖幫我們擋住了并發(fā)穿透。當(dāng)然有人會(huì)問要不要用Redis做分布式鎖我的看法是在這個(gè)量級(jí)下數(shù)據(jù)庫條件更新已經(jīng)足夠穩(wěn)沒必要引入額外的中間件增加運(yùn)維復(fù)雜度。預(yù)約時(shí)段容量控制也是同樣的邏輯。時(shí)段容量存在appointment_slot表中一個(gè)時(shí)段配置了最大容量扣減時(shí)用同樣的條件更新方法。我在設(shè)計(jì)時(shí)把時(shí)段跟批次庫存做了兩層校驗(yàn)避免用戶選了一個(gè)有庫存但沒放號(hào)的時(shí)段。4. 后端核心業(yè)務(wù)實(shí)現(xiàn)4.1 疫苗發(fā)布管理模塊疫苗發(fā)布是管理員的核心操作。我把它拆成兩個(gè)動(dòng)作新增批次入庫和配置預(yù)約活動(dòng)發(fā)布。批次入庫流程管理員選擇疫苗品類填寫批號(hào)、廠家、有效期、數(shù)量、冷鏈溫度提交后系統(tǒng)生成批次記錄并把數(shù)量寫入available_count。這里我做了個(gè)校驗(yàn)同一機(jī)構(gòu)下批號(hào)不允許重復(fù)因?yàn)榕?hào)是追溯的根。配置預(yù)約活動(dòng)是我做得比較細(xì)的地方。管理員選擇一個(gè)批次、設(shè)置可預(yù)約日期范圍、配置每個(gè)日期的時(shí)段容量比如上午50人、下午80人提交后系統(tǒng)自動(dòng)生成一組appointment_slot記錄。這樣發(fā)布的不是一個(gè)籠統(tǒng)的有苗而是清晰到每個(gè)時(shí)段可約多少人的精確計(jì)劃。4.2 預(yù)約下單與鎖庫存的實(shí)現(xiàn)鏈路預(yù)約接口是用戶端最核心的API也是并發(fā)壓力最大的地方。整個(gè)調(diào)用鏈路我分五步校驗(yàn)用戶登錄態(tài)和實(shí)名認(rèn)證狀態(tài)。校驗(yàn)預(yù)約日期是否在活動(dòng)發(fā)布范圍內(nèi)。鎖批次庫存條件更新影響行數(shù)為0則提示疫苗庫存不足。鎖時(shí)段容量條件更新影響行數(shù)為0則釋放步驟3的庫存。生成預(yù)約單狀態(tài)為待確認(rèn)返回預(yù)約單號(hào)。注意到第4步失敗時(shí)必須回滾第3步的庫存。我用Transactional包住這個(gè)流程但這還不夠——如果手動(dòng)用條件更新語句扣庫存事務(wù)回滾只能回滾數(shù)據(jù)庫操作但前提是你要在同一個(gè)事務(wù)方法里完成所有操作。所以我特意把所有扣減邏輯放在一個(gè)事務(wù)方法內(nèi)執(zhí)行保證任何一步拋異常整體回滾。Transactional(rollbackFor Exception.class) public String createAppointment(CreateAppointmentReq req) { // 1. 校驗(yàn)用戶 // 2. 校驗(yàn)活動(dòng)時(shí)間段 // 3. 扣批次庫存 int batchResult vaccineBatchMapper.deductStock(req.getBatchId()); if (batchResult 0) throw new BusinessException(批次庫存不足); // 4. 扣時(shí)段容量 int slotResult appointmentSlotMapper.deductCapacity(req.getSlotId()); if (slotResult 0) throw new BusinessException(該時(shí)段預(yù)約已滿); // 5. 生成預(yù)約單 appointmentMapper.insert(buildAppointment(req)); return appointmentNo; }4.3 超時(shí)取消與庫存釋放的定時(shí)任務(wù)預(yù)約了不接種是真實(shí)世界的常態(tài)。我設(shè)計(jì)了定時(shí)任務(wù)每小時(shí)跑一次把預(yù)約時(shí)間為當(dāng)天之前、狀態(tài)仍為待確認(rèn)或已確認(rèn)的預(yù)約單批量置為已過期同時(shí)釋放對應(yīng)批次和時(shí)段的庫存。這個(gè)定時(shí)任務(wù)的核心難點(diǎn)是批量釋放庫存的正確性。我的實(shí)現(xiàn)方式是先查出所有過期預(yù)約單的批次ID和時(shí)段ID列表然后按預(yù)約單維度逐條恢復(fù)庫存而不是按批次聚合恢復(fù)——這樣可以保證每一單的釋放都能追溯到具體的批次和時(shí)段。另外我給任務(wù)加了分布式鎖的預(yù)留字段防止多實(shí)例部署時(shí)重復(fù)執(zhí)行。4.4 接種核銷與追溯記錄用戶到現(xiàn)場后操作員通過預(yù)約單號(hào)或用戶身份證號(hào)查詢預(yù)約信息確認(rèn)身份后點(diǎn)擊核銷接種。核銷時(shí)做兩件事更新預(yù)約單狀態(tài)為已接種寫入接種記錄表。接種記錄表我單獨(dú)建了一張記錄用戶ID、疫苗批次ID、接種機(jī)構(gòu)、接種時(shí)間、接種第幾劑、接種醫(yī)生、疫苗批號(hào)、留觀狀態(tài)。這個(gè)表的存在讓整條鏈路真正閉環(huán)——從發(fā)布批次到預(yù)約到核銷到追溯任何一環(huán)出了問題都能查到底。5. 前端工程化與核心頁面5.1 Vue項(xiàng)目結(jié)構(gòu)與路由設(shè)計(jì)前端我用Vue2.7 Element UI Vue Router Pinia。雖然是Vue2版本但配合Vite構(gòu)建開發(fā)體驗(yàn)已經(jīng)相當(dāng)順滑。項(xiàng)目結(jié)構(gòu)按模塊劃分src/views頁面視圖分為user用戶端預(yù)約、admin管理后臺(tái)、auth登錄注冊。src/api按業(yè)務(wù)模塊封裝的axios請求方法。src/store用戶信息、機(jī)構(gòu)信息等全局狀態(tài)。路由的設(shè)計(jì)我用了路由守衛(wèi)做權(quán)限控制。管理員路由配置meta: { requiresAdmin: true }用戶在進(jìn)入前從store或本地緩存獲取角色信息不匹配就重定向到首頁。這塊代碼不難但一定要在前端和后端同時(shí)做權(quán)限校驗(yàn)單純的前端守衛(wèi)只是體驗(yàn)層面的后端接口的鑒權(quán)才是安全底線。5.2 預(yù)約流程的前端交互體驗(yàn)用戶端預(yù)約流程我做了四步的向?qū)浇换ミx擇疫苗品類展示該品類下的可用批次和剩余數(shù)量。選擇接種機(jī)構(gòu)和預(yù)約日期此時(shí)會(huì)請求后端接口查出該機(jī)構(gòu)在所選日期的時(shí)段容量。選擇一個(gè)具體時(shí)段點(diǎn)擊預(yù)約提交。預(yù)約成功頁展示預(yù)約單號(hào)和核銷碼。環(huán)節(jié)上有個(gè)細(xì)節(jié)庫存數(shù)量不要用實(shí)時(shí)值展示給用戶。因?yàn)橹灰腥送瑫r(shí)下單頁面顯示的數(shù)字就會(huì)和實(shí)際不一致。我的做法是頁面只顯示剩余10支這種模糊狀態(tài)具體能不能約到以提交時(shí)的接口返回為準(zhǔn)。這樣從產(chǎn)品邏輯上規(guī)避了用戶的明明有苗怎么約不上投訴。5.3 管理后臺(tái)的數(shù)據(jù)看板與報(bào)表管理后臺(tái)我做了三個(gè)重要的頁面疫苗批次管理頁表格展示批次信息、庫存狀態(tài)、操作按鈕發(fā)布預(yù)約、編輯、停用。預(yù)約訂單管理頁支持多條件篩選按機(jī)構(gòu)、疫苗、狀態(tài)、日期支持手動(dòng)取消預(yù)約。統(tǒng)計(jì)報(bào)表頁用ECharts做圖表展示每日預(yù)約量、疫苗接種率、各機(jī)構(gòu)預(yù)約橫向?qū)Ρ?。?bào)表接口是后端比較費(fèi)心思的地方。比如接種率我定義為已接種數(shù) / 已確認(rèn)數(shù)統(tǒng)計(jì)口徑必須跟前端確認(rèn)清楚。尤其是Excel導(dǎo)出功能我用的是EasyExcel在導(dǎo)出前要設(shè)置響應(yīng)頭讓前端觸發(fā)下載這個(gè)屬于老生常談但很容易漏的細(xì)節(jié)。6. 部署、性能優(yōu)化與踩坑記錄6.1 標(biāo)準(zhǔn)部署架構(gòu)由于沒有引入Redis等中間件部署結(jié)構(gòu)非常簡單一臺(tái)Linux服務(wù)器2核4G即可Docker或直接部署SpringBoot jar包MySQL單獨(dú)跑。前端dist目錄由Nginx托管同時(shí)Nginx配置/api前綴的反向代理指向后端服務(wù)的8080端口。前端Nginx配置我附在這里后端跨域問題的根源就是Nginx沒配對server { listen 80; server_name vaccine.example.com; location / { root /opt/vaccine-front/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }6.2 短時(shí)高并發(fā)的性能優(yōu)化疫苗預(yù)約往往集中在發(fā)布后的一小段時(shí)間比如早上9點(diǎn)放號(hào)可能就前幾分鐘流量最大。這個(gè)場景下MySQL的單庫單表能扛住幾千QPS嗎我的實(shí)測經(jīng)驗(yàn)是能但需要做兩件小事。第一是連接池調(diào)優(yōu)。默認(rèn)的HikariCP最大連接數(shù)我調(diào)到了50minimumIdle調(diào)到10避免頻繁創(chuàng)建連接。第二是把寫操作盡量縮短。預(yù)約接口的事務(wù)中所有SQL都走了主鍵索引或唯一索引盡量不用慢查詢。我給時(shí)段容量表加了一個(gè)唯一索引(org_id, slot_date, slot_time)這樣條件更新的行鎖沖突概率大幅降低。6.3 我實(shí)際踩過的幾個(gè)坑坑一事務(wù)內(nèi)遠(yuǎn)程調(diào)用。最開始我在預(yù)約事務(wù)里加入了一個(gè)發(fā)送短信通知的操作遇到短信服務(wù)超時(shí)會(huì)卡住數(shù)據(jù)庫連接池甚至導(dǎo)致整個(gè)預(yù)約流程阻塞。后來我果斷把短信發(fā)送改成異步MQ或線程池處理事務(wù)方法只保留數(shù)據(jù)庫操作。這個(gè)教訓(xùn)很深刻事務(wù)里別干與數(shù)據(jù)庫無關(guān)的事。坑二樂觀鎖扣庫存導(dǎo)致的自增孤兒數(shù)據(jù)。一開始扣庫存失敗后我直接拋異常事務(wù)回滾但會(huì)有一個(gè)問題預(yù)約單的自增ID已經(jīng)分配了回滾后ID出現(xiàn)空洞。這個(gè)其實(shí)不影響業(yè)務(wù)但是復(fù)查數(shù)據(jù)時(shí)看到AUTO_INCREMENT跳號(hào)會(huì)有點(diǎn)慌。有人會(huì)在事務(wù)外先查一次庫存避免多余嘗試但并發(fā)下還是有競態(tài)所以最后我接受了跳號(hào)把精力放在保證業(yè)務(wù)一致性上??尤岸薚ime Zone問題。用戶預(yù)約選擇的是2024-06-15但傳到后端因?yàn)闀r(shí)區(qū)問題變成了前一天晚上16點(diǎn)。排查半天才發(fā)現(xiàn)是Jackson序列化時(shí)區(qū)沒配置。最后在application.yml里統(tǒng)一設(shè)置了spring.jackson.date-format和time-zone: GMT8才解決。這個(gè)問題前端還看不太出來因?yàn)闉g覽器會(huì)自動(dòng)轉(zhuǎn)換顯示只有對數(shù)據(jù)庫記錄時(shí)才會(huì)發(fā)現(xiàn)差了8小時(shí)。坑四MySQL 5.7版本下datetime默認(rèn)值。在創(chuàng)建表時(shí)給create_time字段設(shè)置DEFAULT CURRENT_TIMESTAMP在MySQL 5.7里沒問題但如果在8.0版本會(huì)提示不支持。建議統(tǒng)一用DATETIME(3)加毫秒精度并用Java代碼顯式賦值時(shí)間避免版本兼容問題。6.4 源碼中使用到的關(guān)鍵依賴清單最后列一下后端pom.xml中用到的核心依賴方便你搭環(huán)境時(shí)對照spring-boot-starter-webWeb框架spring-boot-starter-security登錄認(rèn)證與權(quán)限控制mybatis-spring-boot-starter持久層框架mysql-connector-javaMySQL驅(qū)動(dòng)lombok簡化實(shí)體類代碼hibernate-validator參數(shù)校驗(yàn)fastjson2或jacksonJSON序列化java-jwt或jjwt用戶登錄態(tài)令牌alibaba easyexcel報(bào)表導(dǎo)出前端package.json里用到的核心依賴vue 2.7vue-router 3.xpinia 2.xVue2可以用pinia但不支持Vue2的舊項(xiàng)目用了vuexaxioselement-ui 2.15echarts 5.xvite 4.x結(jié)合個(gè)人經(jīng)驗(yàn)做這種企業(yè)級(jí)管理系統(tǒng)最有價(jià)值的不是代碼本身而是對業(yè)務(wù)復(fù)雜度的理解和狀態(tài)流轉(zhuǎn)的把控。疫苗發(fā)布和接種預(yù)約這個(gè)領(lǐng)域看著簡單但批次追溯、時(shí)段容量、并發(fā)扣減、爽約釋放每一環(huán)都藏著真實(shí)世界的坑。如果你拿到這套源碼我建議你先把表結(jié)構(gòu)和狀態(tài)機(jī)讀懂再動(dòng)手改代碼會(huì)比直接上手快得多。