源碼拆解:從接車到結(jié)算的完整業(yè)務(wù)閉環(huán)實(shí)現(xiàn))
簡(jiǎn)介這是一套面向汽車修理廠日常運(yùn)營(yíng)場(chǎng)景的數(shù)字化管理軟件源碼適合計(jì)算機(jī)專業(yè)學(xué)生、課程設(shè)計(jì)開(kāi)發(fā)者以及中小型汽修門店的信息化建設(shè)者參考。系統(tǒng)圍繞客戶檔案、預(yù)約排程、維修訂單、配件庫(kù)存、財(cái)務(wù)收支、員工績(jī)效與統(tǒng)計(jì)報(bào)表等模塊展開(kāi)幫助解決手工記賬混亂、庫(kù)存預(yù)警滯后、經(jīng)營(yíng)數(shù)據(jù)難以匯總等實(shí)際問(wèn)題。資源包共53個(gè)文件約2.66MB以C源碼cpp、h為核心配合MFC工程文件dsp、dsw、clw、編譯中間產(chǎn)物obj、pch、idb以及Access數(shù)據(jù)庫(kù)Car.mdb另含圖標(biāo)、音效與資源腳本構(gòu)成一套可直接編譯運(yùn)行的完整工程。已有678人學(xué)習(xí)下載。通過(guò)閱讀源碼讀者可掌握基于MFC與ADO數(shù)據(jù)庫(kù)訪問(wèn)的桌面管理程序結(jié)構(gòu)理解各功能對(duì)話框與數(shù)據(jù)表的映射關(guān)系并借鑒其模塊劃分與權(quán)限控制思路用于二次開(kāi)發(fā)或畢業(yè)設(shè)計(jì)參考。1. 汽修廠老板的賬本困局這套管理系統(tǒng)到底能解決什么我見(jiàn)過(guò)一個(gè)開(kāi)了八年修理廠的老板接車用粉筆寫(xiě)在白板上配件進(jìn)出靠微信語(yǔ)音月底對(duì)賬時(shí)發(fā)現(xiàn)三輛車修完了沒(méi)收錢。這不是段子是很多中小汽修門店的真實(shí)狀態(tài)。這套「汽車修理廠汽車修理管理系統(tǒng)」就是沖著這個(gè)場(chǎng)景來(lái)的——它把接車登記、維修派工、配件領(lǐng)用、結(jié)算收款串成一條線讓每一輛進(jìn)廠的車都有跡可循。適合誰(shuí)用單店或兩三個(gè)工位的社區(qū)修理廠、快修快保店以及想拿一套完整業(yè)務(wù)系統(tǒng)做二次開(kāi)發(fā)練手的開(kāi)發(fā)者。技術(shù)棧上這類系統(tǒng)常見(jiàn)做法是后端用 Spring Boot 或 Python Flask 提供 REST 接口前端用 Vue 或原生 HTML 加模板渲染數(shù)據(jù)庫(kù)選 MySQL部署在一臺(tái)普通云主機(jī)或門店內(nèi)網(wǎng)機(jī)器上就能跑。下面我按「能干什么、怎么跑起來(lái)、怎么改」的順序把這份源碼拆開(kāi)講。2. 系統(tǒng)架構(gòu)與目錄結(jié)構(gòu)從一張工單看數(shù)據(jù)怎么流轉(zhuǎn)2.1 業(yè)務(wù)閉環(huán)一輛車從進(jìn)廠到結(jié)算經(jīng)過(guò)哪些表先別急著看代碼把業(yè)務(wù)跑通比什么都重要。一輛車進(jìn)廠前臺(tái)登記車牌、車型、車主電話、故障描述系統(tǒng)生成一張維修工單狀態(tài)是「待派工」。車間主管把工單指派給某個(gè)技師狀態(tài)變「維修中」。技師在維修過(guò)程中領(lǐng)用配件系統(tǒng)扣減庫(kù)存并記錄領(lǐng)料明細(xì)。維修完成后工單轉(zhuǎn)「待結(jié)算」前臺(tái)按工時(shí)費(fèi)加配件費(fèi)生成結(jié)算單收款后狀態(tài)變「已結(jié)算」同時(shí)這輛車的維修歷史歸檔下次再來(lái)能直接調(diào)出。這條鏈路對(duì)應(yīng)到數(shù)據(jù)庫(kù)核心是四張表vehicle車輛檔案、repair_order維修工單、part配件庫(kù)存、order_part工單領(lǐng)料明細(xì)。工單表通過(guò)vehicle_id關(guān)聯(lián)車輛領(lǐng)料明細(xì)表通過(guò)order_id和part_id把工單和配件連起來(lái)。搞清這個(gè)關(guān)系后面看任何一段代碼都不會(huì)迷路。-- 維修工單核心表結(jié)構(gòu)簡(jiǎn)化版 CREATE TABLE repair_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 工單號(hào)如 RO20240501001, vehicle_id BIGINT NOT NULL COMMENT 關(guān)聯(lián)車輛, fault_desc VARCHAR(500) COMMENT 故障描述, status TINYINT DEFAULT 0 COMMENT 0待派工 1維修中 2待結(jié)算 3已結(jié)算, technician_id BIGINT COMMENT 指派技師, labor_fee DECIMAL(10,2) DEFAULT 0 COMMENT 工時(shí)費(fèi), part_fee DECIMAL(10,2) DEFAULT 0 COMMENT 配件費(fèi)合計(jì), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );字段說(shuō)明里最關(guān)鍵的是status它驅(qū)動(dòng)了整個(gè)系統(tǒng)的頁(yè)面流轉(zhuǎn)和權(quán)限控制。order_no用日期加流水號(hào)生成方便前臺(tái)口頭報(bào)單號(hào)查進(jìn)度。labor_fee和part_fee分開(kāi)存是因?yàn)榻Y(jié)算時(shí)經(jīng)常要按項(xiàng)目打折分開(kāi)算比混在一起靈活。2.2 目錄結(jié)構(gòu)前后端怎么分配置文件在哪拿到源碼包先看根目錄。這類管理系統(tǒng)常見(jiàn)做法是前后端分離目錄大致長(zhǎng)這樣repair-system/ ├── backend/ # 后端服務(wù) │ ├── src/main/java/... # 控制器、服務(wù)、數(shù)據(jù)訪問(wèn)層 │ ├── src/main/resources/ │ │ ├── application.yml # 數(shù)據(jù)庫(kù)連接、端口配置 │ │ └── mapper/ # MyBatis 的 SQL 映射文件 │ └── pom.xml # Maven 依賴 ├── frontend/ # 前端頁(yè)面 │ ├── src/views/ # 各業(yè)務(wù)頁(yè)面接車、派工、結(jié)算 │ ├── src/api/ # 接口請(qǐng)求封裝 │ └── package.json └── sql/ └── init.sql # 建庫(kù)建表加初始數(shù)據(jù)后端如果用的是 Spring Bootapplication.yml里改數(shù)據(jù)庫(kù)地址和密碼就能連自己的庫(kù)前端src/api目錄下通常有一個(gè)request.js里面配了后端接口的基礎(chǔ)地址部署時(shí)改成實(shí)際 IP 或域名。sql/init.sql是救命文件第一次跑必須先執(zhí)行它否則表都沒(méi)有登錄都進(jìn)不去。2.3 權(quán)限與角色為什么前臺(tái)看不到成本價(jià)系統(tǒng)一般內(nèi)置三種角色管理員、前臺(tái)、技師。管理員能看所有數(shù)據(jù)和報(bào)表前臺(tái)能接車、派工、結(jié)算但看不到配件的進(jìn)價(jià)技師只能看指派給自己的工單和領(lǐng)料。這個(gè)權(quán)限控制在代碼里通??恳粋€(gè)攔截器加角色字段判斷實(shí)現(xiàn)。// 簡(jiǎn)化的權(quán)限攔截邏輯 public boolean checkPermission(String role, String apiPath) { if (ADMIN.equals(role)) return true; // 管理員放行 if (FRONT.equals(role) apiPath.startsWith(/order)) return true; if (TECH.equals(role) apiPath.startsWith(/task)) return true; return false; // 其余一律拒絕 }這段邏輯看著簡(jiǎn)單但它是整個(gè)系統(tǒng)安全感的來(lái)源。參數(shù)role從登錄后寫(xiě)入 session 或 token 里取apiPath是當(dāng)前請(qǐng)求路徑。實(shí)際項(xiàng)目里會(huì)做得更細(xì)比如前臺(tái)能改工單但不能刪工單這些差異都落在checkPermission的分支里。改權(quán)限就是改這里的分支條件比滿項(xiàng)目找if靠譜得多。3. 本地跑起來(lái)數(shù)據(jù)庫(kù)、后端、前端三步走3.1 建庫(kù)導(dǎo)數(shù)據(jù)init.sql 里有什么坑第一步永遠(yuǎn)是數(shù)據(jù)庫(kù)。打開(kāi)sql/init.sql里面通常包含建庫(kù)語(yǔ)句、建表語(yǔ)句和幾條初始數(shù)據(jù)比如默認(rèn)管理員賬號(hào)。執(zhí)行方式有兩種命令行mysql -u root -p init.sql或者用圖形化工具打開(kāi)文件直接運(yùn)行。# 登錄數(shù)據(jù)庫(kù)并執(zhí)行初始化腳本 mysql -u root -p CREATE DATABASE repair_db DEFAULT CHARSET utf8mb4; USE repair_db; SOURCE /path/to/sql/init.sql;這里有個(gè)血淚經(jīng)驗(yàn)字符集一定要用utf8mb4別用utf8。汽修系統(tǒng)里車主姓名、故障描述經(jīng)常出現(xiàn)生僻字utf8存進(jìn)去會(huì)變問(wèn)號(hào)結(jié)算單打出來(lái)車主名字是亂碼客戶當(dāng)場(chǎng)就不信任你了。另外init.sql里的初始密碼通常是明文或簡(jiǎn)單加密第一次登錄后立刻改掉別留著默認(rèn)賬號(hào)跑在公網(wǎng)上。3.2 后端啟動(dòng)改配置、裝依賴、看日志數(shù)據(jù)庫(kù)通了之后進(jìn)backend目錄。如果是 Maven 項(xiàng)目先改application.ymlspring: datasource: url: jdbc:mysql://127.0.0.1:3306/repair_db?useUnicodetruecharacterEncodingutf8mb4 username: root password: 你的密碼 server: port: 8080url里的characterEncodingutf8mb4和數(shù)據(jù)庫(kù)字符集對(duì)應(yīng)別漏。然后執(zhí)行mvn clean package打包或者直接在 IDE 里跑主啟動(dòng)類。啟動(dòng)成功的標(biāo)志是日志最后出現(xiàn)Started Application in x seconds并且沒(méi)有Communications link failure這類數(shù)據(jù)庫(kù)連接報(bào)錯(cuò)。# 打包并運(yùn)行 mvn clean package -DskipTests java -jar target/repair-system-1.0.jar-DskipTests是跳過(guò)測(cè)試加快打包第一次跑建議加上等系統(tǒng)跑通了再回頭跑測(cè)試。如果啟動(dòng)報(bào)端口占用改server.port就行別去殺進(jìn)程容易誤傷別的服務(wù)。3.3 前端啟動(dòng)接口地址別寫(xiě)死前端進(jìn)frontend目錄先npm install裝依賴再npm run dev起開(kāi)發(fā)服務(wù)器。關(guān)鍵動(dòng)作是找到接口基礎(chǔ)地址的配置通常在.env.development或src/api/request.js里。// src/api/request.js 里的基礎(chǔ)地址配置 const baseURL process.env.VUE_APP_BASE_API || http://127.0.0.1:8080; // 開(kāi)發(fā)環(huán)境指向本地后端生產(chǎn)環(huán)境改成實(shí)際服務(wù)器地址很多人本地跑不通就是卡在這后端在 8080前端請(qǐng)求卻發(fā)到了別的端口瀏覽器控制臺(tái)一片 404。改完地址重啟前端用初始管理員賬號(hào)登錄能看到接車頁(yè)面就說(shuō)明整條鏈路通了。這時(shí)候別急著錄入真實(shí)數(shù)據(jù)先隨便建一輛測(cè)試車走一遍完整流程確認(rèn)工單狀態(tài)能正常流轉(zhuǎn)。4. 核心模塊拆解接車、派工、結(jié)算三段代碼4.1 接車登記工單號(hào)怎么生成才不重復(fù)接車是整個(gè)系統(tǒng)的入口代碼質(zhì)量直接影響后面所有環(huán)節(jié)。核心是生成一個(gè)唯一工單號(hào)并落庫(kù)。// 工單號(hào)生成日期 4位當(dāng)日流水 public String generateOrderNo() { String datePart LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); // 查詢當(dāng)天已有多少工單加一作為流水號(hào) int todayCount orderMapper.countByDate(datePart); return RO datePart String.format(%04d, todayCount 1); }邏輯說(shuō)明datePart取當(dāng)天日期countByDate查當(dāng)天工單數(shù)量加一后補(bǔ)零成四位。參數(shù)上%04d保證流水號(hào)至少四位當(dāng)天超過(guò) 9999 單才會(huì)溢出中小修理廠根本到不了。這里有個(gè)并發(fā)隱患兩個(gè)人同時(shí)接車可能拿到同一個(gè)流水號(hào)。常見(jiàn)做法是給order_no加唯一索引插入失敗就重試一次或者直接用數(shù)據(jù)庫(kù)自增 ID 拼日期更省心。4.2 派工與領(lǐng)料庫(kù)存扣減必須和工單綁定派工是把工單指派給技師領(lǐng)料是技師從倉(cāng)庫(kù)拿配件。這兩步必須在一個(gè)事務(wù)里完成否則會(huì)出現(xiàn)「配件領(lǐng)走了但工單沒(méi)記錄」的糊涂賬。Transactional // 事務(wù)注解保證扣庫(kù)存和寫(xiě)明細(xì)同時(shí)成功或同時(shí)失敗 public void assignAndPickParts(Long orderId, Long technicianId, ListPickItem items) { // 1. 更新工單狀態(tài)和技師 orderMapper.updateStatusAndTech(orderId, 1, technicianId); // 2. 逐項(xiàng)扣減庫(kù)存并寫(xiě)領(lǐng)料明細(xì) for (PickItem item : items) { int affected partMapper.reduceStock(item.getPartId(), item.getQty()); if (affected 0) { throw new RuntimeException(配件庫(kù)存不足 item.getPartId()); } orderPartMapper.insert(orderId, item.getPartId(), item.getQty()); } }Transactional是這段代碼的靈魂。reduceStock的 SQL 里帶WHERE stock #{qty}條件庫(kù)存不夠時(shí)返回影響行數(shù) 0直接拋異常回滾整個(gè)操作。參數(shù)items是技師勾選的配件列表每項(xiàng)含配件 ID 和數(shù)量。這樣即使中途某個(gè)配件不夠前面扣掉的庫(kù)存也會(huì)回滾不會(huì)出現(xiàn)半截賬。4.3 結(jié)算收款工時(shí)費(fèi)和配件費(fèi)怎么算準(zhǔn)結(jié)算要把工時(shí)費(fèi)和配件費(fèi)匯總生成最終金額。配件費(fèi)從領(lǐng)料明細(xì)里按當(dāng)時(shí)售價(jià)累加工時(shí)費(fèi)按工單上記錄的金額取。-- 匯總某工單的配件費(fèi) SELECT IFNULL(SUM(op.qty * p.sale_price), 0) AS part_fee FROM order_part op JOIN part p ON op.part_id p.id WHERE op.order_id #{orderId};IFNULL是防止沒(méi)有領(lǐng)料時(shí)返回 null導(dǎo)致前端顯示空白。sale_price取的是配件表里的當(dāng)前售價(jià)這里有個(gè)細(xì)節(jié)如果配件調(diào)過(guò)價(jià)歷史工單的配件費(fèi)會(huì)跟著變。嚴(yán)謹(jǐn)做法是在order_part表里冗余一個(gè)unit_price字段領(lǐng)料時(shí)把當(dāng)時(shí)售價(jià)存進(jìn)去結(jié)算就按這個(gè)存下來(lái)的價(jià)格算不受后續(xù)調(diào)價(jià)影響。這是很多新手會(huì)翻車的地方改起來(lái)不難加個(gè)字段的事。5. 避坑與常見(jiàn)問(wèn)題跑不起來(lái)先看這幾條5.1 啟動(dòng)報(bào)數(shù)據(jù)庫(kù)連接失敗現(xiàn)象后端啟動(dòng)日志里出現(xiàn)Communications link failure或Access denied for user。原因通常是application.yml里的地址、端口、賬號(hào)密碼和實(shí)際數(shù)據(jù)庫(kù)對(duì)不上或者數(shù)據(jù)庫(kù)服務(wù)根本沒(méi)啟動(dòng)。解決先用命令行mysql -u root -p確認(rèn)能連上再核對(duì)配置文件里的每一項(xiàng)特別注意密碼里有沒(méi)有特殊字符需要轉(zhuǎn)義。5.2 頁(yè)面能打開(kāi)但接口全 404現(xiàn)象前端登錄頁(yè)面正常顯示點(diǎn)登錄沒(méi)反應(yīng)瀏覽器控制臺(tái)一堆 404。原因基本是前端接口基礎(chǔ)地址配錯(cuò)了或者后端沒(méi)起在預(yù)期端口。解決打開(kāi)瀏覽器開(kāi)發(fā)者工具的 Network 面板看請(qǐng)求實(shí)際發(fā)到了哪個(gè)地址和request.js里的baseURL對(duì)比改成一致即可。5.3 中文亂碼現(xiàn)象錄入的車主姓名、故障描述存進(jìn)數(shù)據(jù)庫(kù)變成問(wèn)號(hào)或者頁(yè)面顯示亂碼。原因有三處可能數(shù)據(jù)庫(kù)字符集不是utf8mb4、連接串沒(méi)加characterEncoding、前端頁(yè)面沒(méi)聲明 UTF-8。解決按數(shù)據(jù)庫(kù)、連接串、頁(yè)面三層逐一排查建庫(kù)時(shí)就用utf8mb4最省事。5.4 工單狀態(tài)卡住不流轉(zhuǎn)現(xiàn)象派工后工單還是顯示「待派工」或者結(jié)算后狀態(tài)沒(méi)變。原因多半是狀態(tài)更新的 SQL 條件寫(xiě)死了舊狀態(tài)比如WHERE status 0但實(shí)際狀態(tài)已經(jīng)不是 0 了。解決檢查更新語(yǔ)句的WHERE條件確認(rèn)它和當(dāng)前實(shí)際狀態(tài)匹配或者干脆只按id更新?tīng)顟B(tài)值由業(yè)務(wù)代碼控制。5.5 配件庫(kù)存扣成負(fù)數(shù)現(xiàn)象庫(kù)存表里出現(xiàn)負(fù)庫(kù)存。原因是扣減時(shí)沒(méi)加庫(kù)存充足判斷或者并發(fā)下兩個(gè)請(qǐng)求同時(shí)扣。解決扣減 SQL 必須帶WHERE stock #{qty}并在業(yè)務(wù)層判斷影響行數(shù)為 0 就拋異常回滾。高并發(fā)場(chǎng)景再加數(shù)據(jù)庫(kù)行鎖或樂(lè)觀鎖版本號(hào)。6. 二次開(kāi)發(fā)從改一個(gè)字段到加一張報(bào)表拿到這套源碼最值錢的不是原樣跑起來(lái)而是按自己門店的規(guī)矩改。我一般從三個(gè)方向下手按難度遞增。第一層改字段和文案。比如工單表想加一個(gè)「預(yù)計(jì)交車時(shí)間」字段改動(dòng)路徑是init.sql加列、實(shí)體類加屬性、Mapper 的 insert 和 update 語(yǔ)句加參數(shù)、前端表單加輸入框。四步缺一不可漏了哪步都會(huì)出現(xiàn)「填了不保存」或「保存了不顯示」。這類改動(dòng)最安全適合先練手熟悉代碼結(jié)構(gòu)。第二層加業(yè)務(wù)規(guī)則。比如「結(jié)算時(shí)必須先確認(rèn)所有領(lǐng)料都已歸還或核銷」。做法是在結(jié)算接口里加一段校驗(yàn)查order_part表里有沒(méi)有未核銷的記錄有就返回錯(cuò)誤提示。這類改動(dòng)要?jiǎng)?Service 層改完務(wù)必用測(cè)試數(shù)據(jù)走一遍完整流程別只測(cè)單個(gè)接口。第三層加報(bào)表和統(tǒng)計(jì)。修理廠老板最想看的是「這個(gè)月修了多少車、哪個(gè)技師產(chǎn)值最高、哪些配件周轉(zhuǎn)最快」。加報(bào)表的思路是新建一個(gè)統(tǒng)計(jì)接口用 SQL 聚合查詢出數(shù)據(jù)前端加一個(gè)圖表頁(yè)面。下面是一個(gè)技師產(chǎn)值的統(tǒng)計(jì)示例-- 按月統(tǒng)計(jì)各技師完成的工單數(shù)和工時(shí)費(fèi)合計(jì) SELECT t.name AS technician, COUNT(o.id) AS order_count, IFNULL(SUM(o.labor_fee), 0) AS total_labor FROM repair_order o JOIN technician t ON o.technician_id t.id WHERE o.status 3 -- 只統(tǒng)計(jì)已結(jié)算工單 AND DATE_FORMAT(o.created_at, %Y-%m) #{month} GROUP BY t.id, t.name ORDER BY total_labor DESC;status 3是關(guān)鍵過(guò)濾條件只算已結(jié)算的才算真實(shí)產(chǎn)值維修中的工單不能計(jì)入。DATE_FORMAT按月份分組前端傳2024-05這樣的參數(shù)就能查指定月份。這個(gè)查詢?cè)跀?shù)據(jù)量大的時(shí)候會(huì)慢常見(jiàn)做法是給created_at和status加聯(lián)合索引。改造時(shí)有個(gè)習(xí)慣我一直保持每次改完數(shù)據(jù)庫(kù)結(jié)構(gòu)先手動(dòng)執(zhí)行一遍建表語(yǔ)句確認(rèn)無(wú)誤再重啟后端最后清瀏覽器緩存刷新前端。這三步走完再測(cè)功能能省掉大量「明明改了卻沒(méi)生效」的玄學(xué)時(shí)間。有次我改了個(gè)字段類型沒(méi)同步改實(shí)體類排查了半小時(shí)才發(fā)現(xiàn)是類型不匹配導(dǎo)致插入靜默失敗從那以后我每次動(dòng)表結(jié)構(gòu)都強(qiáng)制走一遍「SQL、實(shí)體、Mapper、前端」四件套核對(duì)。希望這套拆解能幫你少走點(diǎn)彎路把這份源碼真正變成自己門店能用的系統(tǒng)。本文還有配套的精品資源點(diǎn)擊獲取