:從智能報價引擎到業(yè)務(wù)閉環(huán)實現(xiàn))
我做畢業(yè)設(shè)計的時候選了個挺有意思的題目基于SpringBoot的窗簾報價管理系統(tǒng)正式一點的名字叫“云簾”軟裝布藝報價及銷售管理系統(tǒng)。班里大部分同學(xué)都在做商城、博客、校園二手交易我卻挑了這么個垂直行業(yè)的小項目當(dāng)時還被人問“窗簾有什么好做的”。做下來才發(fā)現(xiàn)這個選題比想象中值錢得多窗簾報價不是簡單地增刪改查它背后有一套復(fù)雜的業(yè)務(wù)規(guī)則——不同窗型、不同面料、不同褶皺倍率報價結(jié)果能差出好幾倍。系統(tǒng)最初版本叫“飛卷”取意窗簾成卷、報價飛快后來擴(kuò)展成覆蓋報價、訂單、生產(chǎn)、收款全流程的“云簾”平臺。這個系統(tǒng)解決的是軟裝門店最現(xiàn)實的問題手工量窗、Excel報價、口頭砍價、紙質(zhì)訂單效率低不說報價口徑還不統(tǒng)一同一個客戶兩次報價可能差出幾百塊。系統(tǒng)核心是一個智能報價引擎錄入窗戶尺寸選擇面料和款式自動算出用料米數(shù)、輔料費(fèi)用和總價確認(rèn)報價后轉(zhuǎn)入訂單再往下走生產(chǎn)、安裝、收款形成完整業(yè)務(wù)閉環(huán)。如果你是做Java畢設(shè)、想找SpringBoot項目切入點的同學(xué)或者本身在幫傳統(tǒng)門店做信息化改造這篇文章應(yīng)該都派得上用場。我會把項目從業(yè)務(wù)拆解、表設(shè)計、核心算法到權(quán)限控制和踩坑實錄完整過一遍。1. 業(yè)務(wù)分析與項目定位窗簾報價為什么值得做一個系統(tǒng)1.1 一間軟裝門店的日常困境先還原一個真實場景??蛻暨M(jìn)店說家里三室兩廳需要給客廳落地窗、主臥飄窗、次臥平開窗和書房轉(zhuǎn)角窗配窗簾。銷售員要做的事包括去現(xiàn)場量每扇窗的寬度和高度問清楚客戶的遮光需求、風(fēng)格偏好帶客戶翻面料冊子雪尼爾、高精密、真絲棉、遮光布每米價格從幾十到幾百確定是裝羅馬桿還是靜音軌道再考慮要不要加水波簾頭、花邊、掛球、鉛墜。這一套下來人工算價至少二三十分鐘而且很容易漏項。漏了一個軌道報價就得返工算錯一個褶皺倍率門店就要自己貼錢。更麻煩的是報價口徑不統(tǒng)一。同一個窗不同銷售員可能用不同的褶皺倍率給客戶報出來的總價能差幾百塊。老板想月底看哪個品類賣得好、哪類窗型利潤高翻Excel要翻半天還經(jīng)常發(fā)現(xiàn)數(shù)據(jù)對不上。窗簾這個品類有一個特點它不像普通商品一樣有固定標(biāo)價每一單都得按尺寸和配置單獨算價這就天然需要一個“計算型”的系統(tǒng)而不是普通的商品展示加購物車。1.2 兩個名字與一條主鏈路的由來我在標(biāo)題里寫了兩個名字“飛卷”和“云簾”。其實它們是同一個項目的兩個階段。初版只做報價計算叫“飛卷”意思是輸入尺寸、一鍵出價報價單像窗簾一樣“卷”出來。后來把訂單、生產(chǎn)、收款、報表都加進(jìn)來改名叫“云簾”面向的是整個軟裝布藝門店的銷售管理。這種命名在畢設(shè)題目里很常見建議大家在論文里把名字的由來寫清楚答辯時也是一個不錯的引入點。系統(tǒng)要服務(wù)三類角色店長關(guān)注全店銷售數(shù)據(jù)、利潤和折扣審批銷售員負(fù)責(zé)接待、量窗、錄報價、跟單財務(wù)或庫管負(fù)責(zé)訂單審核、生產(chǎn)安排和收款登記。主鏈路就一句話客戶管理 - 量窗記錄 - 智能報價 - 折扣審批 - 確認(rèn)訂單 - 按窗排產(chǎn) - 安裝交付 - 收款對賬 - 銷售報表。這個閉環(huán)就是整個系統(tǒng)的骨架后面的表設(shè)計、接口設(shè)計都圍繞它展開。1.3 為什么不用通用商城模板也有人在選題時糾結(jié)過直接套一個電商商城模板改改商品和訂單不就行了實際做下來就知道差別在哪。標(biāo)準(zhǔn)電商是“標(biāo)價購買”商品價格是靜態(tài)的窗簾是“定制計算”每個訂單都要重新算價。窗簾報價的輸入是窗戶尺寸、面料、款式、輔料、折扣輸出是明細(xì)項、合計金額和利潤估算跟商城完全不是一個模型。ERP系統(tǒng)通常太重一套下來成本幾十萬門店根本用不起所以這種輕量的垂直報價系統(tǒng)正好卡在中間比Excel專業(yè)比ERP便宜。對畢設(shè)來說這反而給了一個很好的展示空間你可以在系統(tǒng)里植入行業(yè)算法、策略模式、狀態(tài)機(jī)這些有深度的設(shè)計而不是千篇一律的CRUD。2. 技術(shù)選型與工程結(jié)構(gòu)SpringBoot 3 MyBatis Plus MySQL2.1 后端技術(shù)棧與選型理由先上完整的技術(shù)清單都是我實際驗證過能跑通的組合。技術(shù)版本/選型理由JDK17SpringBoot 3 基線版本長期支持穩(wěn)定SpringBoot3.2.x自動配置強(qiáng)大內(nèi)嵌Tomcat適合業(yè)務(wù)系統(tǒng)MyBatis Plus3.5.x單表CRUD零SQL復(fù)雜查詢用LambdaQueryWrapperMySQL8.0關(guān)系型業(yè)務(wù)數(shù)據(jù)事務(wù)能力強(qiáng)Redis6.x/7.x登錄狀態(tài)、字典緩存Vue 3 Element Plus前端表單、表格組件成熟適合管理后臺EasyExcel3.x導(dǎo)出報價單、銷售報表省內(nèi)存MinIO可選布料圖片、報價單附件對象存儲為什么持久層選 MyBatis Plus 而不是 Spring Data JPA我的理由是報價系統(tǒng)里多表關(guān)聯(lián)查詢非常多比如報價單主表加明細(xì)加客戶加審批記錄MyBatis Plus 的 QueryWrapper 和自定義 XML 都很順手JPA 在復(fù)雜查詢時要么寫JPQL要么寫原生SQL對很多人來說學(xué)習(xí)成本反而更高。MyBatis Plus 還提供邏輯刪除、樂觀鎖、自動填充這些開箱即用的功能對畢設(shè)項目來說能省出大量時間。2.2 單模塊還是多模塊包結(jié)構(gòu)怎么分畢設(shè)項目我強(qiáng)烈建議用單模塊夠用且好演示。真正的關(guān)鍵是把包結(jié)構(gòu)設(shè)計清楚做到邏輯上的分層。我的工程包結(jié)構(gòu)是這樣com.yunlian ├── controller // 接口層只做參數(shù)接收與響應(yīng)包裝 ├── service // 業(yè)務(wù)層核心計算、事務(wù)都在這里 ├── mapper // 數(shù)據(jù)訪問層繼承BaseMapper ├── entity // 數(shù)據(jù)庫實體字段與表一一對應(yīng) ├── dto // 入?yún)ο蠼邮涨岸苏埱髤?shù) ├── vo // 出參對象返回前端展示數(shù)據(jù) ├── common // 統(tǒng)一響應(yīng)、分頁對象、常量 ├── config // 配置類如Redis、攔截器、Jackson ├── utils // 工具類如價格引擎、Excel導(dǎo)出 └── exception // 全局異常與自定義業(yè)務(wù)異常這個結(jié)構(gòu)看起來常規(guī)但有兩個細(xì)節(jié)值得注意。第一VO 和 DTO 必須和 Entity 分離Entity 里的 createTime、updateTime、deleted 這些字段絕不能直接暴露給前端報價單的計算中間結(jié)果比如每個明細(xì)的折扣前金額、折扣后金額、節(jié)省金額都要在 VO 中定義好而不是從 Entity 里隨便取字段拼。第二Controller 要瘦、Service 要厚。所有業(yè)務(wù)判斷都寫在 Service 層Controller 只做參數(shù)綁定和調(diào)用這樣也方便在 Service 上直接加事務(wù)和權(quán)限注解。2.3 統(tǒng)一響應(yīng)與全局異常后端接口的“標(biāo)準(zhǔn)格式”前后端分離項目里接口返回格式必須統(tǒng)一。我封裝了一個 Result 結(jié)構(gòu)是 code、message、data 三段式code 為 200 表示成功其他為失敗或特殊狀態(tài)碼。效果就是每個 Controller 方法直接返回 Result.success(業(yè)務(wù)數(shù)據(jù))不需要每個接口各自拼 JSON。配合 ResultCode 枚舉把錯誤碼集中管理比如 400 參數(shù)錯誤、401 未登錄、403 無權(quán)限、500 系統(tǒng)異常、1001 報價單已失效這樣前后端排查問題效率高很多。全局異常處理是保證接口穩(wěn)定的關(guān)鍵。用 RestControllerAdvice 加 ExceptionHandler 做統(tǒng)一出口業(yè)務(wù)異常拋出 BusinessException參數(shù)校驗異常拋出 MethodArgumentNotValidException數(shù)據(jù)庫異常單獨處理。特別是數(shù)據(jù)庫唯一鍵沖突如果不單獨捕獲前端收到的堆棧信息又長又嚇人捕獲后會返回“該手機(jī)號已存在”這樣友好的提示。這部分在答辯時基本都會被問到建議好好準(zhǔn)備。3. 數(shù)據(jù)庫設(shè)計一套報價單如何拆成六張核心表3.1 表結(jié)構(gòu)總覽窗簾報價系統(tǒng)的表可以分成三組基礎(chǔ)數(shù)據(jù)表、業(yè)務(wù)單據(jù)表和系統(tǒng)權(quán)限表?;A(chǔ)數(shù)據(jù)包括客戶、面料、輔料、窗戶信息業(yè)務(wù)單據(jù)包括報價單、報價明細(xì)、訂單、生產(chǎn)記錄、收款記錄權(quán)限表就是用戶、角色、用戶角色關(guān)聯(lián)。一張報價單從錄入到成交路徑大致是先建客戶檔案再錄量窗信息然后基于面料和輔料生成報價單主表和明細(xì)客戶下單后轉(zhuǎn)成訂單訂單按窗戶拆成生產(chǎn)任務(wù)安裝完成后登記收款。表之間的關(guān)系用一句話概括客戶1對多報價單報價單1對多明細(xì)報價單1對1轉(zhuǎn)訂單訂單1對多生產(chǎn)任務(wù)。3.2 核心表結(jié)構(gòu)與關(guān)鍵字段下面給出報價和訂單最核心的幾張表設(shè)計字段以實際項目為準(zhǔn)做了精簡。CREATE TABLE quote ( id bigint NOT NULL AUTO_INCREMENT, quote_no varchar(32) NOT NULL COMMENT 報價單號規(guī)則QDyyyyMMdd序號, customer_id bigint NOT NULL COMMENT 客戶ID, customer_name varchar(64) DEFAULT NULL COMMENT 冗余客戶姓名列表不join, total_fabric_amount decimal(10,2) NOT NULL COMMENT 面料金額, total_accessory_amount decimal(10,2) NOT NULL COMMENT 輔料金額, total_amount decimal(10,2) NOT NULL COMMENT 總價, discount_amount decimal(10,2) NOT NULL COMMENT 優(yōu)惠金額, final_amount decimal(10,2) NOT NULL COMMENT 應(yīng)收金額, status tinyint NOT NULL COMMENT 0草稿 1已報價 2已確認(rèn) 3已轉(zhuǎn)訂單 4已失效, sales_user_id bigint NOT NULL COMMENT 銷售員ID, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, deleted tinyint DEFAULT 0, PRIMARY KEY (id), KEY idx_customer_id (customer_id), KEY idx_sales_user_id (sales_user_id) ) ENGINEInnoDB COMMENT報價單主表; CREATE TABLE quote_item ( id bigint NOT NULL AUTO_INCREMENT, quote_id bigint NOT NULL, window_id bigint NOT NULL COMMENT 窗戶ID, fabric_id bigint NOT NULL, fabric_name varchar(64) DEFAULT NULL, window_width decimal(8,2) NOT NULL COMMENT 窗寬單位米, window_height decimal(8,2) NOT NULL COMMENT 窗高單位米, fold_ratio decimal(4,2) NOT NULL DEFAULT 2.00 COMMENT 褶皺倍率, piece_count int NOT NULL COMMENT 幅數(shù), fabric_meters decimal(10,2) NOT NULL COMMENT 面料用量米數(shù), unit_price decimal(10,2) NOT NULL COMMENT 面料單價, fabric_amount decimal(10,2) NOT NULL COMMENT 面料金額, accessory_amount decimal(10,2) NOT NULL COMMENT 輔料金額, subtotal decimal(10,2) NOT NULL COMMENT 小計, PRIMARY KEY (id), KEY idx_quote_id (quote_id) ) ENGINEInnoDB COMMENT報價明細(xì)表;這段 SQL 里有幾個設(shè)計決定值得說道。第一個是金額和尺寸全部用 decimal金額用 decimal(10,2)尺寸用 decimal(8,2)單位統(tǒng)一是米而不用厘米避免前端展示和計算時到處換算。第二個是客戶姓名、面料名稱做了冗余列表頁展示時減少聯(lián)表查詢這是典型的空間換時間。第三個是狀態(tài)字段用 tinyint 加代碼里的枚舉映射不要在數(shù)據(jù)庫里直接存中文否則改狀態(tài)名要改庫而且接口返回也要做轉(zhuǎn)換。3.3 面料與輔料報價的“價格基礎(chǔ)數(shù)據(jù)”報價引擎要計算就離不開面料和輔料兩張基礎(chǔ)表。面料表 fabric 的關(guān)鍵字段包括面料名稱、類別遮光布/雪尼爾/高精密/絨布/真絲棉等、單價、計價方式按米還是按平米、布幅寬、損耗率、顏色/花紋描述、圖片URL。輔料表 accessory 的關(guān)鍵字段包括名稱羅馬桿/靜音軌道/水波簾頭/花邊/掛球/鉛墜等、單位、單價、是否按長度為計費(fèi)單位。這里要注意有的輔料按米算有的按個算所以輔料表里加一個 charge_type 字段區(qū)分是按長度計費(fèi)還是按數(shù)量計費(fèi)價格引擎在匯總時會按不同方式處理。面料價格是會波動的所以我在面料表里加了一個 price_version 字段。每次改價不直接update舊記錄而是插入一條新版本記錄報價單明細(xì)里會冗余當(dāng)時的 unitPrice。這樣做的直接好處是一個月后客戶來問“我當(dāng)時這個面料多少錢”系統(tǒng)里能查到歷史報價快照。這本身也是報價系統(tǒng)比Excel強(qiáng)很多的地方——Excel在布料漲價后覆蓋保存歷史價格就徹底沒了。4. 智能報價引擎這個項目最有技術(shù)含量的部分4.1 窗簾用料的行業(yè)計算邏輯窗簾報價最核心的公式不是簡單的“長乘寬”而是由一系列行業(yè)規(guī)則組成的計算鏈條。先把關(guān)鍵參數(shù)說清楚窗寬 W實際量得的窗戶寬度單位米窗高 H實際量得的窗戶高度單位米褶皺倍率 K一般取 1.82.2決定了窗簾掛起來的褶皺效果。遮光布通常取 1.8紗簾取 2.0高檔絨布可能取 2.2布幅寬 F面料門幅常見 1.5m 定寬、2.8m 定高折邊量 S上下卷邊、包邊增加的米數(shù)習(xí)慣取 0.2m計算思路分兩種情況。如果是“定高布”也就是面料幅寬 2.8m 作為高度方向買寬用料寬度 窗寬 × 褶皺倍率再考慮拼接總米數(shù) 用料寬度 / 布幅寬向上取整× 布幅寬實際就是按整幅買。如果是“定寬布”面料 1.5m 是寬度方向先算幅數(shù) ceil(用料寬度 / 1.5)再算每幅需要的高度 窗高 折邊量最終面料米數(shù) 幅數(shù) × 每幅高度。這個“幅數(shù)”概念是窗簾報價和普通面積計算最大的區(qū)別也是報價引擎里最容易出bug的地方。為了讓大家看得更直觀我舉個具體例子。一扇窗寬 3.2m、高 2.6m做 2.0 倍褶皺用 1.5m 定寬布。用料寬度 3.2 × 2.0 6.4m幅數(shù) ceil(6.4 / 1.5) 5 幅。每幅高度 2.6 0.2 2.8m。面料總米數(shù) 5 × 2.8 14m。如果這布單價 68 元/米面料費(fèi)用就是 14 × 68 952 元。再加軌道費(fèi)用 窗寬 × 軌道單價比如 3.2 × 30 96 元。這扇窗的基礎(chǔ)報價就是 1048 元。系統(tǒng)里把每一步中間結(jié)果都存到明細(xì)表客戶問起來可以逐項解釋。4.2 價格引擎的代碼實現(xiàn)與 BigDecimal 取整這套邏輯落到代碼里我用了一個獨立的 CurtainCalcEngine 類輸入?yún)?shù)封裝成 QuoteCalcParam輸出封裝成 QuoteCalcResult。核心計算片段如下public QuoteCalcResult calc(QuoteCalcParam param) { BigDecimal width param.getWindowWidth(); BigDecimal height param.getWindowHeight(); BigDecimal ratio param.getFoldRatio(); BigDecimal fabricWidth param.getFabricWidth(); // 1. 計算需要的布料寬度 BigDecimal needWidth width.multiply(ratio); // 2. 計算幅數(shù)向上取整 BigDecimal pieceCount needWidth.divide(fabricWidth, 0, RoundingMode.CEILING); // 3. 每幅高度 窗高 折邊量 BigDecimal heightPerPiece height.add(HEM_AMOUNT); // 4. 面料總米數(shù) BigDecimal totalMeters pieceCount.multiply(heightPerPiece); // 5. 面料金額 BigDecimal fabricAmount totalMeters.multiply(param.getUnitPrice()); // 6. 輔料金額軌道按窗寬計費(fèi) BigDecimal railAmount width.multiply(param.getRailPrice()); // 7. 小計 BigDecimal total fabricAmount.add(railAmount); return QuoteCalcResult.builder() .pieceCount(pieceCount.intValue()) .fabricMeters(totalMeters) .fabricAmount(fabricAmount) .railAmount(railAmount) .totalAmount(total) .build(); }這段代碼里有幾個坑都是我實際踩過的先說最典型的兩個。第一個是除法必須指定舍入模式。BigDecimal 的 divide 如果不指定精度和 RoundingMode遇到除不盡的情況直接拋 ArithmeticExceptionnon-terminating decimal expansion。我第一次跑測試數(shù)據(jù)時裝了一個窗寬 1.1m 的用例算幅數(shù) 1.1×2.0/1.51.4666...系統(tǒng)當(dāng)場崩了。改成 divide(fabricWidth, 0, RoundingMode.CEILING) 后結(jié)果是向上取整到 2 幅這才符合行業(yè)習(xí)慣——布料只能多買不能少買。第二個是金額計算的精度控制。單價是 decimal(10,2)算出來的金額可能是 68 × 14 952.00看起來沒毛病但如果讓系統(tǒng)保留多位小數(shù)再傳到前端就會出現(xiàn) 952.00000001 這種顯示。建議所有金額計算最后都要調(diào) setScale(2, RoundingMode.HALF_UP)。我在金額字段的 setter 處沒有做統(tǒng)一處理而是在每個引擎入口出口都有一步“收攏精度”的操作這個習(xí)慣比到處用 DecimalFormat 靠譜得多。4.3 階梯折扣與審批流報價系統(tǒng)的“生意感”真實門店不會按標(biāo)價賣東西系統(tǒng)必須支持折扣。我設(shè)計了 DiscountStrategy 策略接口具體的實現(xiàn)有三類普通客戶不打折用量達(dá)到 30 米以上享受 95 折50 米以上 9 折會員在折扣基礎(chǔ)上再 95 折。為什么用策略模式因為門店促銷活動三個月就會調(diào)一次如果把這些判斷用 if-else 堆在報價 Service 里每次調(diào)策略都要改業(yè)務(wù)代碼改完還要重新測試整個流程。用策略模式每種折扣是一個獨立的類新增一種活動就新增一個類部署時通過配置中心或數(shù)據(jù)庫字典切換既不碰舊邏輯也方便回滾。折扣還要配合審批流。我做了這樣的規(guī)則銷售員默認(rèn)最多能給 95 折想給 9 折必須申請店長審批超過 8.5 折則要店長加財務(wù)雙重審批。審批記錄表 approve_record 保存報價單ID、申請理由、申請金額、審批人、審批意見、審批時間。報價單的最終應(yīng)收金額必須從“審批通過后的價格”重新計算而不是銷售員在前端隨便填否則審批就形同虛設(shè)。像這類規(guī)則在畢設(shè)論文里寫清楚設(shè)計動機(jī)比堆技術(shù)點更有說服力。5. 訂單管理與狀態(tài)流轉(zhuǎn)從報價到交付的完整閉環(huán)5.1 狀態(tài)機(jī)把訂單流程“鎖死”報價經(jīng)過客戶簽字確認(rèn)之后就轉(zhuǎn)成正式訂單。訂單狀態(tài)我定義了六個待確認(rèn)、待生產(chǎn)、生產(chǎn)中、待安裝、已完成、已取消。從待確認(rèn)到生產(chǎn)中的遷移并不是任何狀態(tài)下都能跳的比如待生產(chǎn)才能改交貨日期已完成才能發(fā)起售后已取消不能再回到生產(chǎn)中。如果只用一個 status 字段到處 set狀態(tài)很容易被繞亂。我用一個狀態(tài)機(jī)配置類來管理合法遷移private static final MapOrderStatus, SetOrderStatus TRANSITIONS new HashMap(); static { TRANSITIONS.put(OrderStatus.PENDING_CONFIRM, Set.of(OrderStatus.PENDING_PRODUCE, OrderStatus.CANCELED)); TRANSITIONS.put(OrderStatus.PENDING_PRODUCE, Set.of(OrderStatus.PRODUCING, OrderStatus.CANCELED)); TRANSITIONS.put(OrderStatus.PRODUCING, Set.of(OrderStatus.PENDING_INSTALL)); TRANSITIONS.put(OrderStatus.PENDING_INSTALL, Set.of(OrderStatus.COMPLETED)); }所有狀態(tài)遷移都在一個 service 方法里統(tǒng)一校驗非法遷移直接拋 BusinessException“訂單當(dāng)前狀態(tài)不允許該操作”。這樣做的好處是不管以后從后臺還是APP進(jìn)來改狀態(tài)都必須走同一個入口規(guī)則不會出現(xiàn)兩套。答辯時把狀態(tài)機(jī)畫出來評委一眼就能看到你對業(yè)務(wù)閉環(huán)的理解。注意我這里強(qiáng)調(diào)“畫出來”是指論文里的示意圖不是項目必須輸出的圖表組件。5.2 按“扇”排產(chǎn)訂單子表的細(xì)節(jié)設(shè)計一個訂單可能包含好幾扇窗戶而工廠生產(chǎn)是按扇推進(jìn)的。整單一起排產(chǎn)有個現(xiàn)實問題落地窗和飄窗可能不在同一天加工完成客戶收到貨的時間也不一樣。所以我在訂單下建了一張 order_window 子表記錄訂單里每一扇窗戶對應(yīng)的尺寸、面料、褶皺倍率、生產(chǎn)狀態(tài)、安裝日期。主訂單狀態(tài)是匯總值子表各自推進(jìn)自己的生產(chǎn)狀態(tài)。這樣銷售員在后臺可以看到“這單里落地窗已經(jīng)在安裝了飄窗還在排產(chǎn)”而不是籠統(tǒng)地看到一個“生產(chǎn)中”。這張子表讓我在演示時非常加分。我做了個訂單詳情頁左側(cè)是訂單主信息右側(cè)是一扇窗一張卡每張卡上有布料圖片、尺寸、狀態(tài)、預(yù)計安裝時間。你現(xiàn)場演示的時候點一張卡的“開始生產(chǎn)”整個頁面狀態(tài)跟著變比純列表直觀得多。5.3 收款記錄與銷售報表訂單完成后進(jìn)來的是錢。收款表 payment_record 記錄每筆收款訂單號、收款類型定金/尾款/退款、支付方式現(xiàn)金/微信/支付寶/刷卡、金額、收款人、時間。財務(wù)對賬時按天匯總導(dǎo)出 Excel 清單用 EasyExcel 實現(xiàn)。相比直接用 POIEasyExcel 在導(dǎo)出大數(shù)據(jù)量時的內(nèi)存占用低很多而且支持填模板導(dǎo)出生成一套帶格式的《門店日銷售匯總表》很方便。關(guān)于報表我做了一個銷售看板按日/按月統(tǒng)計訂單數(shù)、銷售額、退款額按面料分類統(tǒng)計銷量占比按銷售員排行。圖表用 ECharts 渲染后端接口返回的是聚合查詢結(jié)果。這里有一個實用的優(yōu)化報表查詢走獨立的統(tǒng)計 SQL而不是把訂單全部查出來在內(nèi)存里算因為訂單多了內(nèi)存統(tǒng)計會慢到?jīng)]法看。6. 權(quán)限設(shè)計店長、銷售員、財務(wù)各看各的數(shù)據(jù)6.1 RBAC 角色權(quán)限模型系統(tǒng)有角色就有權(quán)限問題。我用 Spring Security JWT 做認(rèn)證和授權(quán)權(quán)限模型是最標(biāo)準(zhǔn)的 RBAC用戶表、角色表、用戶角色關(guān)聯(lián)表。三個初始角色店長、銷售員、財務(wù)/庫管。接口層用 PreAuthorize 控制訪問比如刪除報價單只允許店長操作創(chuàng)建報價單銷售員和店長都可以導(dǎo)出財務(wù)報表只有財務(wù)和店長可以。前端菜單也做了權(quán)限控制不同角色登錄后看到的菜單項不同這個用 Vue Router 的動態(tài)路由實現(xiàn)。JWT 本身是無狀態(tài)的但畢設(shè)項目里我加了一點“有狀態(tài)”的處理把 JWT 的 tokenId 存到 Redis設(shè)置過期時間用戶修改密碼或賬號被禁用時把 tokenId 拉進(jìn)黑名單。這樣既保留 JWT 的輕量又能解決“改密碼后舊 token 還能用”的尷尬。這個設(shè)計在面試或答辯時能聊很久。6.2 行級數(shù)據(jù)權(quán)限同一個角色看到的數(shù)據(jù)也得分RBAC 解決的是“誰能訪問這個接口”但解決不了“銷售員張三不應(yīng)該看到銷售員李四的報價單”。這種按數(shù)據(jù)行劃分的權(quán)限叫數(shù)據(jù)權(quán)限。我在 MyBatis Plus 的查詢層做了處理用一個自定義攔截器讀取當(dāng)前登錄用戶信息執(zhí)行報價單列表查詢時自動拼接條件。銷售員只能查 user_id 自己的數(shù)據(jù)店長不加限制財務(wù)按部門或全店范圍查。實現(xiàn)上有個細(xì)節(jié)攔截器里拼接條件容易讓 SQL 變得不可控尤其跟分頁、排序組合時。我的做法是只有在特定方法的 Mapper 接口上才啟用數(shù)據(jù)權(quán)限攔截器不是全局生效。在畢設(shè)項目里重點把這個邏輯講清楚就夠了為什么需要行級權(quán)限、怎么用攔截器實現(xiàn)、怎么避免誤傷其它查詢。這個問題是答辯的高頻追問提前想好答案能省很多現(xiàn)場卡殼的風(fēng)險。6.3 文件存儲布料圖片用 MinIO 還是本地磁盤系統(tǒng)里每款布料要有平鋪圖和效果圖報價單詳情頁要能展示布料小圖。圖片上傳的存儲方案畢設(shè)里常見的有兩種存本地磁盤路徑、存對象存儲。如果項目要部署在服務(wù)器上演示本地磁盤是最省事的配置一個虛擬路徑映射即可。但熱門技術(shù)詞里也有人提到 springboot 整合 minio如果你的畢設(shè)想展示企業(yè)級技術(shù)棧把圖片上傳到 MinIO 是很好的加分項。MinIO 的接口兼容 S3 協(xié)議Spring Boot 里集成很簡單上傳后返回一個可訪問的 object URL前端直接img展示。我的建議是如果時間充裕用 MinIO如果趕時間本地磁盤 Nginx 映射完全夠用。不要為了炫技術(shù)把項目復(fù)雜化重點是業(yè)務(wù)閉環(huán)能跑通。布料圖片的 URL 存到 fabric 表里報價明細(xì)里冗余一張縮略圖 URL這樣列表頁不需要再 join 面料表取圖片。7. 實戰(zhàn)踩坑記錄與答辯準(zhǔn)備7.1 我踩過的四個大坑第一個坑是金額精度。早期我用 double 類型存金額測試時 0.1 0.2 不等于 0.3 這種經(jīng)典問題直接出現(xiàn)在報價單上。后來全部改成 BigDecimal前端傳過來的金額字符串也用 BigDecimal 構(gòu)造堅決不用 Double.parseDouble。凡是涉及金額的相加、相乘、折扣必須走 BigDecimal 并指定舍入模式這個習(xí)慣我建議從第一天就養(yǎng)成。第二個坑是 LocalDateTime 的序列化。Spring Boot 默認(rèn)把 LocalDateTime 序列化成數(shù)組格式前端拿到 2024,5,20,15,30,0 一臉懵。統(tǒng)一在 Jackson 配置里注冊 JavaTimeModule并且設(shè)置格式為 yyyy-MM-dd HH:mm:ss。這個配置很小如果不處理前后端時間顯示就會出各種奇奇怪怪的格式。第三個坑是 MyBatis Plus 邏輯刪除和唯一索引沖突。我給客戶表加了 deleted 字段做邏輯刪除同時又給手機(jī)號字段建了唯一索引。結(jié)果用戶 A 刪除了再新增一個同手機(jī)號的用戶 B數(shù)據(jù)庫直接報唯一鍵沖突——因為邏輯刪除的記錄還在表里。解決方案是把 deleted 字段放進(jìn)聯(lián)合唯一索引也就是 UNIQUE KEY uk_mobile_deleted (mobile, deleted)這樣不同 deleted 值的記錄不算重復(fù)。這個坑比較隱蔽網(wǎng)上資料不算多踩過一次就很酸爽。第四個坑是列表頁的 N1 查詢。報價單列表頁如果每行都要查一次明細(xì)去算金額100 條報價單就是 1 100 條 SQL。后來改成先批量查詢所有報價單再用 in 查詢所有明細(xì)在內(nèi)存里按 quoteId 分組組裝。數(shù)據(jù)量上來后這個優(yōu)化的效果體感非常明顯。7.2 常見問題速查表問題現(xiàn)象可能原因解決方式報價單算出來的金額比預(yù)期高褶皺倍率設(shè)置過高或幅數(shù)向上取整檢查面料幅寬與倍率配置窗簾是整幅銷售不能按小數(shù)買BigDecimal 除法報 ArithmeticException未指定舍入模式divide 必須帶 scale 和 RoundingMode前端時間顯示成數(shù)組LocalDateTime 未做序列化配置注冊 Jackson JavaTimeModule設(shè)置時間格式刪除客戶后無法新增同手機(jī)號客戶邏輯刪除與唯一索引沖突聯(lián)合唯一索引加上 deleted 字段改了密碼舊 token 仍有效JWT 無狀態(tài)未做服務(wù)端校驗Redis 黑名單或版本號校驗列表頁接口非常慢循環(huán)查詢明細(xì)的 N1 問題批量查詢 內(nèi)存分組7.3 答辯演示準(zhǔn)備數(shù)據(jù)與話術(shù)做完項目一定要準(zhǔn)備一套完整的演示數(shù)據(jù)而且最好是一套有“故事感”的數(shù)據(jù)。我的演示數(shù)據(jù)是一戶三室兩廳業(yè)主客廳落地窗、主臥飄窗、次臥平開窗選了兩款遮光布和一款紗簾報價 8000 多然后打折審批、轉(zhuǎn)訂單、排產(chǎn)、收款、看報表。整套流程走下來五分鐘能把系統(tǒng)所有核心能力都覆蓋到。我見過不少同學(xué)答辯時現(xiàn)場臨時錄數(shù)據(jù)錄到一半報錯或者數(shù)據(jù)太少看不了報表場面非常尷尬。提前把演示腳本寫好每一步點哪里、用什么臺詞解釋至少走兩遍。話術(shù)上有一個建議不要只說“我用了 SpringBoot 和 MyBatis Plus”要說“這個系統(tǒng)的核心是一個報價引擎它把窗簾行業(yè)的幅數(shù)計算和褶皺倍率固化成可配置參數(shù)再結(jié)合策略模式實現(xiàn)折扣體系”。技術(shù)點要落到業(yè)務(wù)上評委聽到的不是名詞而是你解決問題的思路。做這個項目給我最大的體會是垂直行業(yè)的小系統(tǒng)最值錢的不是框架版本多新而是你有沒有真正吃透業(yè)務(wù)規(guī)則。我把報價引擎反復(fù)算了三遍拿家里真實窗簾尺寸驗證發(fā)現(xiàn)手工算和系統(tǒng)算能對上時才覺得這個系統(tǒng)真的“活”了。如果你正準(zhǔn)備做類似的畢設(shè)項目我的建議是先找一間真實窗簾門店聊一小時記錄他們的報價流程和一張真實的報價單這比對著文檔設(shè)計三天都管用。有了業(yè)務(wù)細(xì)節(jié)表結(jié)構(gòu)、算法、界面、甚至論文目錄都會自己長出來。