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