實(shí)戰(zhàn):公司用車管理系統(tǒng)源碼設(shè)計(jì)與避坑指南)
簡(jiǎn)介這份資源是南昌航空大學(xué)軟件學(xué)院21級(jí)Web大作業(yè)的完整項(xiàng)目源碼主題為公司用車管理系統(tǒng)面向正在完成課程設(shè)計(jì)或需要Java Web實(shí)戰(zhàn)案例的在校學(xué)生與初學(xué)者。項(xiàng)目采用Java、HTML、JavaScript和CSS等技術(shù)棧開發(fā)涵蓋申請(qǐng)用車、司機(jī)請(qǐng)假、車輛調(diào)度、后臺(tái)管理等典型業(yè)務(wù)模塊并附有數(shù)據(jù)庫(kù)視圖代碼、接口設(shè)計(jì)與SQL語(yǔ)句便于理解前后端交互與數(shù)據(jù)表設(shè)計(jì)思路。壓縮包共175個(gè)文件約914KB其中66個(gè)Java源文件承載核心業(yè)務(wù)邏輯30個(gè)HTML頁(yè)面與19個(gè)JavaScript腳本、16個(gè)CSS樣式表共同構(gòu)建前端界面另有XML配置、JSP頁(yè)面、PNG圖片及Markdown說明文檔等目錄結(jié)構(gòu)清晰適合按模塊拆解學(xué)習(xí)。目前已有343人學(xué)習(xí)下載可作為課程作業(yè)參考、功能擴(kuò)展或二次開發(fā)的起點(diǎn)幫助讀者快速掌握J(rèn)ava Web項(xiàng)目的組織方式與實(shí)現(xiàn)細(xì)節(jié)。1. 從一份 Java Web 大作業(yè)說起公司用車管理系統(tǒng)到底要解決什么如果你正在搜「基于Java的南昌航空大學(xué)軟件學(xué)院21級(jí)web大作業(yè)公司用車管理系統(tǒng)設(shè)計(jì)源碼」大概率是三種人之一要交課程設(shè)計(jì)的學(xué)生、想拿一套完整 Java Web 項(xiàng)目練手的自學(xué)者、或者需要給內(nèi)部做車輛調(diào)度小工具的一線開發(fā)。這三類人訴求其實(shí)高度重合——要一套能跑起來、結(jié)構(gòu)清楚、能改能擴(kuò)的源碼而不是一個(gè)只能截圖交差的空殼。公司用車管理系統(tǒng)聽起來像「管理信息系統(tǒng)」里最普通的一類但它恰好把 Java Web 的核心鏈路全踩了一遍登錄鑒權(quán)、角色權(quán)限、車輛臺(tái)賬、用車申請(qǐng)與審批、狀態(tài)流轉(zhuǎn)、數(shù)據(jù)統(tǒng)計(jì)。它不花哨卻足夠真實(shí)真實(shí)到你把權(quán)限和狀態(tài)機(jī)寫錯(cuò)第二天就會(huì)有人抱怨「車明明還回來了系統(tǒng)里還顯示占用」。這篇筆記就順著這個(gè)標(biāo)題把這類系統(tǒng)的領(lǐng)域邊界、技術(shù)選型、落地步驟和血淚踩坑講清楚讓你拿到源碼能看懂看懂之后能自己改出一版能用的東西。2. 公司用車管理系統(tǒng)的領(lǐng)域拆解與技術(shù)選型2.1 先把業(yè)務(wù)對(duì)象理清車、人、單、狀態(tài)很多人一上來就打開 IDE 建 Spring Boot 工程結(jié)果寫到一半發(fā)現(xiàn)字段對(duì)不上。公司用車管理系統(tǒng)的領(lǐng)域模型其實(shí)就四塊車輛Vehicle、用戶User、用車申請(qǐng)單Apply、審批與調(diào)度記錄Record。車輛有車牌、車型、座位數(shù)、當(dāng)前狀態(tài)空閑/占用/維保/停用用戶分普通員工、部門審批人、管理員三種角色申請(qǐng)單承載「誰、什么時(shí)候、去哪、幾個(gè)人、為什么」記錄則負(fù)責(zé)把「申請(qǐng)—審批—派車—?dú)w還」這條鏈路的每一步留痕。這里最容易翻車的是狀態(tài)字段。新手常把「申請(qǐng)狀態(tài)」和「車輛狀態(tài)」混成一個(gè)字段導(dǎo)致審批通過后車還沒出庫(kù)就被標(biāo)記成占用。正確做法是兩張表各管各的申請(qǐng)單狀態(tài)走待審批 → 已通過 → 已派車 → 已完成 / 已駁回車輛狀態(tài)走空閑 → 占用 → 空閑兩者通過派車動(dòng)作關(guān)聯(lián)。這個(gè)拆分想清楚后面寫代碼基本不會(huì)返工。2.2 技術(shù)棧怎么選別為了炫技上微服務(wù)課程設(shè)計(jì)和大作業(yè)場(chǎng)景下最穩(wěn)的組合是 Spring Boot MyBatis-Plus MySQL Thymeleaf 或 Vue 前后端分離。為什么不是 Spring Cloud因?yàn)橐惶子密嚬芾硐到y(tǒng)的并發(fā)量可能一天幾十次上微服務(wù)只會(huì)讓你在注冊(cè)中心、網(wǎng)關(guān)、鏈路追蹤上耗掉大半時(shí)間最后業(yè)務(wù)沒寫完。常見做法是單體分層Controller 接請(qǐng)求、Service 寫業(yè)務(wù)、Mapper 管數(shù)據(jù)、Entity 對(duì)應(yīng)表。前端如果時(shí)間緊Thymeleaf 服務(wù)端渲染最快出效果想練前后端分離Vue3 Axios 也完全夠用。數(shù)據(jù)庫(kù)選 MySQL 8字符集用utf8mb4別用默認(rèn)的 latin1否則中文姓名和備注全是問號(hào)。連接池用 HikariCPSpring Boot 默認(rèn)自帶配置里把maximum-pool-size設(shè)成 10 到 20 就夠課程設(shè)計(jì)環(huán)境根本壓不滿。下面是一個(gè)最小可用的application.yml片段參數(shù)都標(biāo)了含義。spring: datasource: url: jdbc:mysql://localhost:3306/vehicle_mgmt?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 15 # 連接池上限課程設(shè)計(jì) 10~20 足夠 minimum-idle: 5 # 常駐空閑連接避免頻繁創(chuàng)建 connection-timeout: 30000 # 獲取連接超時(shí) 30s thymeleaf: cache: false # 開發(fā)期關(guān)緩存改頁(yè)面不用重啟 mybatis-plus: configuration: map-underscore-to-camel-case: true # 數(shù)據(jù)庫(kù)下劃線自動(dòng)映射 Java 駝峰 global-config: db-config: logic-delete-field: deleted # 邏輯刪除字段 logic-delete-value: 1 logic-not-delete-value: 0serverTimezone必須顯式指定否則 MySQL 8 驅(qū)動(dòng)會(huì)報(bào)時(shí)區(qū)錯(cuò)誤這是新手第一個(gè)卡點(diǎn)。map-underscore-to-camel-case打開后vehicle_no自動(dòng)映射到vehicleNo省掉大量Results注解。邏輯刪除字段建議一開始就加上車輛和申請(qǐng)單都不該物理刪除否則歷史記錄對(duì)不上賬。2.3 權(quán)限模型三角色 攔截器就夠別硬上 Shiro課程設(shè)計(jì)里權(quán)限做兩件事登錄校驗(yàn)和角色區(qū)分。用 Spring 攔截器HandlerInterceptor配合 Session 是最輕的方案不需要引入 Spring Security 那套復(fù)雜配置。核心思路是登錄成功后把userId和role寫進(jìn) Session攔截器判斷請(qǐng)求路徑前綴/admin/**只放管理員/approve/**放審批人和管理員其余登錄即可訪問。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { HttpSession session req.getSession(false); if (session null || session.getAttribute(userId) null) { resp.sendRedirect(/login); // 未登錄統(tǒng)一跳登錄頁(yè) return false; } String role (String) session.getAttribute(role); String uri req.getRequestURI(); if (uri.startsWith(/admin) !ADMIN.equals(role)) { resp.sendError(403, 無權(quán)限); // 非管理員訪問管理接口直接 403 return false; } return true; } }攔截器注冊(cè)時(shí)用addPathPatterns(/**).excludePathPatterns(/login, /css/**, /js/**)把靜態(tài)資源和登錄頁(yè)放行。這里有個(gè)細(xì)節(jié)getSession(false)不主動(dòng)創(chuàng)建 Session避免未登錄請(qǐng)求也生成一堆空 Session。角色判斷用字符串比較雖然土但課程設(shè)計(jì)夠用真要擴(kuò)展再換成枚舉或權(quán)限表。3. 從建表到跑通一套可復(fù)現(xiàn)的落地步驟3.1 數(shù)據(jù)庫(kù)設(shè)計(jì)五張核心表與關(guān)鍵字段表結(jié)構(gòu)定錯(cuò)后面全是補(bǔ)丁。核心五張表sys_user、vehicle、apply_order、approve_record、driver可選如果涉及司機(jī)分配。下面給出關(guān)鍵字段和類型直接照著建能省半天。表名關(guān)鍵字段類型說明sys_userid, username, password, real_name, role, deptbigint/varcharrole 存 ADMIN/APPROVER/STAFFvehicleid, vehicle_no, model, seats, status, remarkbigint/varchar/intstatus 存 0空閑 1占用 2維保 3停用apply_orderid, user_id, vehicle_id, destination, start_time, end_time, reason, statusbigint/datetime/varcharstatus 存 0待審 1通過 2派車 3完成 4駁回approve_recordid, order_id, approver_id, result, comment, create_timebigint/varchar每次審批留一條可追溯driverid, name, phone, statusbigint/varchar需要司機(jī)時(shí)關(guān)聯(lián)建表時(shí)給vehicle_no加唯一索引給apply_order的user_id和status加普通索引。時(shí)間字段統(tǒng)一用datetime別用timestamp后者有 2038 問題和時(shí)區(qū)隱式轉(zhuǎn)換課程設(shè)計(jì)里容易出玄學(xué) bug。CREATE TABLE vehicle ( id BIGINT PRIMARY KEY AUTO_INCREMENT, vehicle_no VARCHAR(20) NOT NULL UNIQUE COMMENT 車牌號(hào), model VARCHAR(50) COMMENT 車型, seats INT DEFAULT 5 COMMENT 座位數(shù), status TINYINT DEFAULT 0 COMMENT 0空閑 1占用 2維保 3停用, deleted TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;deleted字段配合 MyBatis-Plus 的邏輯刪除查詢自動(dòng)過濾已刪數(shù)據(jù)。create_time用默認(rèn)值插入時(shí)不用手動(dòng)賦值。3.2 用車申請(qǐng)與審批的狀態(tài)流轉(zhuǎn)實(shí)現(xiàn)狀態(tài)流轉(zhuǎn)是這類系統(tǒng)的靈魂也是最容易寫出并發(fā)問題的地方。核心邏輯員工提交申請(qǐng)status0審批人通過status1管理員派車時(shí)把車輛 status 置為 1 占用、申請(qǐng)單置為 2歸還時(shí)兩者復(fù)位。這里必須用數(shù)據(jù)庫(kù)事務(wù)包住「改申請(qǐng)單 改車輛」兩個(gè)操作否則一個(gè)成功一個(gè)失敗數(shù)據(jù)就臟了。Service public class ApplyService { Autowired private ApplyOrderMapper orderMapper; Autowired private VehicleMapper vehicleMapper; Transactional(rollbackFor Exception.class) // 任一失敗整體回滾 public void dispatch(Long orderId, Long vehicleId) { ApplyOrder order orderMapper.selectById(orderId); if (order.getStatus() ! 1) { throw new BizException(只有已通過的申請(qǐng)才能派車); // 狀態(tài)前置校驗(yàn) } Vehicle v vehicleMapper.selectById(vehicleId); if (v.getStatus() ! 0) { throw new BizException(車輛當(dāng)前不可用); } order.setStatus(2); order.setVehicleId(vehicleId); orderMapper.updateById(order); v.setStatus(1); vehicleMapper.updateById(v); } }Transactional的rollbackFor Exception.class很關(guān)鍵默認(rèn)只回滾運(yùn)行時(shí)異常業(yè)務(wù)異常如果不顯式聲明不會(huì)回滾。狀態(tài)前置校驗(yàn)必須放在最前面防止重復(fù)派車。真實(shí)場(chǎng)景里還要考慮「同一輛車同一時(shí)間段不能被兩單占用」這需要在派車前查時(shí)間重疊屬于進(jìn)階校驗(yàn)課程設(shè)計(jì)可以先做基礎(chǔ)版。3.3 車輛臺(tái)賬的增刪改查與分頁(yè)臺(tái)賬管理是使用頻率最高的模塊用 MyBatis-Plus 的分頁(yè)插件能省掉手寫 limit。先注冊(cè)分頁(yè)攔截器再在 Service 里調(diào)page()。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }public IPageVehicle pageQuery(int pageNum, int pageSize, String keyword) { LambdaQueryWrapperVehicle wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(Vehicle::getVehicleNo, keyword) .or().like(Vehicle::getModel, keyword); // 車牌或車型模糊匹配 } wrapper.orderByDesc(Vehicle::getCreateTime); return vehicleMapper.selectPage(new Page(pageNum, pageSize), wrapper); }pageNum從 1 開始pageSize建議限制在 10 到 50前端傳參時(shí)做一次上限校驗(yàn)防止有人傳 10000 把庫(kù)拖垮。like查詢?cè)跀?shù)據(jù)量大時(shí)會(huì)慢但課程設(shè)計(jì)數(shù)據(jù)量小可接受真要優(yōu)化就上全文索引或搜索引擎那是另一個(gè)話題。4. 避坑與排查那些讓大作業(yè)返工的細(xì)節(jié)4.1 中文亂碼從數(shù)據(jù)庫(kù)到頁(yè)面的三層排查現(xiàn)象頁(yè)面顯示「????」或「?μ?èˉ?」。原因通常有三層數(shù)據(jù)庫(kù)字符集不是 utf8mb4、連接 URL 沒帶 characterEncoding、Tomcat 響應(yīng)編碼沒設(shè)。解決順序是先SHOW VARIABLES LIKE character%確認(rèn)庫(kù)和表都是 utf8mb4再檢查 JDBC URL 是否帶characterEncodingutf8mb4最后在application.yml里加server.servlet.encoding.charsetutf8mb4和forcetrue。三層都對(duì)了還亂碼檢查 IDE 的文件編碼是不是 GBK。4.2 時(shí)間字段差 8 小時(shí)時(shí)區(qū)配置的連鎖反應(yīng)現(xiàn)象存進(jìn)去的時(shí)間和顯示的時(shí)間差 8 小時(shí)。原因是 MySQL 驅(qū)動(dòng)默認(rèn)用 UTC而服務(wù)器在東八區(qū)。解決JDBC URL 加serverTimezoneAsia/Shanghai同時(shí)確認(rèn) MySQL 全局時(shí)區(qū)SELECT global.time_zone。如果用了timestamp字段還要注意它隨時(shí)區(qū)自動(dòng)轉(zhuǎn)換所以前面建議統(tǒng)一用datetime。這個(gè)坑不解決審批時(shí)間、派車時(shí)間全亂對(duì)賬時(shí)能讓你懷疑人生。4.3 邏輯刪除后唯一索引沖突現(xiàn)象刪掉車牌「贛A12345」后再新增同車牌報(bào)唯一索引沖突。原因是邏輯刪除只是把deleted置 1記錄還在表里唯一索引依然生效。解決把唯一索引改成(vehicle_no, deleted)聯(lián)合唯一或者刪除時(shí)把車牌改成贛A12345_刪除時(shí)間戳。前者更規(guī)范后者更省事課程設(shè)計(jì)里兩種都常見。4.4 事務(wù)不生效自調(diào)用與異常類型現(xiàn)象派車時(shí)車輛狀態(tài)改了申請(qǐng)單沒改事務(wù)沒回滾。原因通常是 Service 方法內(nèi)部自調(diào)用this.dispatch()Spring AOP 代理失效。解決把事務(wù)方法拆到另一個(gè) Bean或者注入自身代理。另一個(gè)原因是拋了檢查異常但沒配rollbackFor前面代碼里已經(jīng)強(qiáng)調(diào)過。排查時(shí)在事務(wù)方法里手動(dòng)拋異??磾?shù)據(jù)是否回滾是最快的驗(yàn)證手段。4.5 前端傳參與后端接收對(duì)不上現(xiàn)象表單提交后后端收到 null。原因多是字段名不一致比如前端傳vehicleNo后端實(shí)體是vehicle_no又沒開駝峰映射。解決統(tǒng)一用駝峰命名或在application.yml打開map-underscore-to-camel-case。另外RequestBody和RequestParam混用也會(huì)導(dǎo)致收不到JSON 提交用前者表單提交用后者別搞反。5. 讓這套源碼真正值錢的三個(gè)進(jìn)階技巧第一把狀態(tài)流轉(zhuǎn)做成可配置的狀態(tài)機(jī)?,F(xiàn)在狀態(tài)是硬編碼的if-else加一個(gè)「取消申請(qǐng)」?fàn)顟B(tài)就要改一堆地方??梢杂妹杜e定義狀態(tài)和允許的遷移比如PENDING.canTransferTo(APPROVED, REJECTED)校驗(yàn)時(shí)統(tǒng)一走canTransferTo擴(kuò)展時(shí)只改枚舉。第二給關(guān)鍵操作加操作日志。派車、審批、刪除車輛這些動(dòng)作往operation_log表插一條「誰、什么時(shí)候、干了什么、結(jié)果如何」出問題時(shí)能追溯這也是真實(shí)企業(yè)系統(tǒng)的基本要求。第三把重復(fù)的權(quán)限校驗(yàn)抽成注解?,F(xiàn)在每個(gè) Controller 方法里手寫角色判斷改成自定義RequireRole(ADMIN)注解加攔截器統(tǒng)一處理代碼干凈一大截。驗(yàn)證這套系統(tǒng)是否真的跑通別只看頁(yè)面能點(diǎn)。按這個(gè)順序走一遍管理員登錄建車、員工登錄提交申請(qǐng)、審批人登錄通過、管理員派車、員工確認(rèn)歸還、管理員看車輛狀態(tài)是否復(fù)位。每一步都去數(shù)據(jù)庫(kù)SELECT一下對(duì)應(yīng)字段確認(rèn)狀態(tài)和時(shí)間都對(duì)。我自己的習(xí)慣是每次改完?duì)顟B(tài)相關(guān)代碼先把這條鏈路手動(dòng)跑三遍比寫單元測(cè)試還快。這套東西不復(fù)雜但細(xì)節(jié)密度高能完整跑通一遍你對(duì) Java Web 的 CRUD、事務(wù)、權(quán)限、狀態(tài)管理的理解就落地了。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取