貿(mào)市場(chǎng)管理系統(tǒng)畢業(yè)設(shè)計(jì)全流程解析)
又到了畢業(yè)設(shè)計(jì)選題的時(shí)候我最近被問(wèn)到最多的問(wèn)題就是SpringBoot農(nóng)貿(mào)市場(chǎng)管理系統(tǒng)這種題目是不是太簡(jiǎn)單了做出來(lái)會(huì)不會(huì)顯得沒(méi)有技術(shù)含量說(shuō)實(shí)話兩年前我自己接觸這個(gè)方向時(shí)也有同樣的疑慮但真正把一個(gè)基于SpringBoot的農(nóng)產(chǎn)品在線交易平臺(tái)從零搭起來(lái)之后才發(fā)現(xiàn)這個(gè)題目的水很深——它表面是個(gè)“管理系統(tǒng)”實(shí)際上要求你在一套B/S架構(gòu)里跑通生鮮電商的完整鏈路商品管理、購(gòu)物車、訂單、庫(kù)存、支付、統(tǒng)計(jì)分析全都要有。它的業(yè)務(wù)復(fù)雜度介于“圖書管理”和“電商秒殺”之間既不會(huì)讓你無(wú)從下手又能把SpringBoot生態(tài)里的核心知識(shí)點(diǎn)全部串起來(lái)是非常典型的畢業(yè)設(shè)計(jì)優(yōu)質(zhì)選題。這篇文章我會(huì)把整個(gè)項(xiàng)目的落地過(guò)程拆開(kāi)來(lái)講內(nèi)容包括需求邊界怎么圈定、技術(shù)棧怎么選型、數(shù)據(jù)庫(kù)表怎么設(shè)計(jì)、核心交易鏈路怎么實(shí)現(xiàn)、開(kāi)發(fā)過(guò)程中最容易被扣分的幾個(gè)坑以及最后論文和答辯怎么準(zhǔn)備。不管你是想直接參考這個(gè)題目還是打算在它基礎(chǔ)上換個(gè)場(chǎng)景比如社區(qū)團(tuán)購(gòu)、校園二手交易這套思路都能復(fù)用。1. 三個(gè)關(guān)鍵詞幫我圈定了需求邊界1.1 系統(tǒng)里到底有哪幾類角色剛開(kāi)始拿到這個(gè)題目時(shí)最容易犯的錯(cuò)誤是上來(lái)就寫代碼。我見(jiàn)過(guò)一位同學(xué)第一版把系統(tǒng)做成了“商戶后臺(tái)管理后臺(tái)”兩個(gè)界面消費(fèi)者端的瀏覽和下單功能完全沒(méi)有答辯時(shí)被導(dǎo)師問(wèn)了一句“你的農(nóng)產(chǎn)品賣給誰(shuí)”當(dāng)場(chǎng)就沒(méi)接上話。這里要先把標(biāo)題里的三個(gè)關(guān)鍵詞拆開(kāi)理解“農(nóng)貿(mào)市場(chǎng)管理系統(tǒng)”強(qiáng)調(diào)的是線下市場(chǎng)管理側(cè)比如攤位商戶管理、市場(chǎng)公告、投訴建議、交易數(shù)據(jù)匯總?!稗r(nóng)產(chǎn)品在線交易平臺(tái)”強(qiáng)調(diào)線上交易側(cè)消費(fèi)者可以逛、加購(gòu)物車、下單?!吧r電商服務(wù)系統(tǒng)”強(qiáng)調(diào)業(yè)務(wù)的電商屬性訂單狀態(tài)流轉(zhuǎn)、庫(kù)存扣減、支付流程要設(shè)計(jì)得像樣。把這三個(gè)維度合并后系統(tǒng)的角色應(yīng)該是四類角色核心訴求對(duì)應(yīng)功能消費(fèi)者在線買菜、查詢訂單商品瀏覽、購(gòu)物車、下單、模擬支付、訂單跟蹤商戶/攤主上架商品、管理庫(kù)存商品管理、庫(kù)存修改、訂單發(fā)貨市場(chǎng)運(yùn)營(yíng)管理員維護(hù)市場(chǎng)秩序、發(fā)布公告商戶入駐審核、商品審核、公告管理、數(shù)據(jù)統(tǒng)計(jì)系統(tǒng)管理員平臺(tái)運(yùn)維用戶管理、角色權(quán)限、系統(tǒng)配置我在項(xiàng)目里最終實(shí)現(xiàn)的版本中消費(fèi)者走C端頁(yè)面商戶和管理員共用一套后臺(tái)管理界面通過(guò)角色權(quán)限來(lái)控制菜單和數(shù)據(jù)范圍。這樣做的好處是頁(yè)面數(shù)量不會(huì)失控同時(shí)又能體現(xiàn)“不同角色看到不同內(nèi)容”的權(quán)限設(shè)計(jì)思路。1.2 哪些功能必須做哪些可以直接砍掉畢業(yè)設(shè)計(jì)不是商業(yè)項(xiàng)目最忌諱“功能堆砌”。我在需求階段列了一個(gè)優(yōu)先級(jí)清單原則是先保證主鏈路完整再考慮錦上添花。必須做的主鏈路功能用戶注冊(cè)登錄手機(jī)號(hào)密碼JWT鑒權(quán)商品分類瀏覽和關(guān)鍵字搜索購(gòu)物車增刪改查下單生成訂單、扣庫(kù)存模擬支付回調(diào)支付結(jié)果、更新訂單狀態(tài)商戶端商品上下架、庫(kù)存維護(hù)后臺(tái)用戶管理、公告管理基礎(chǔ)數(shù)據(jù)統(tǒng)計(jì)訂單量、銷售額、商品銷量Top10可以后期擴(kuò)展的功能論文里寫成“未來(lái)展望”即可優(yōu)惠券和滿減活動(dòng)菜品推薦算法配送路線規(guī)劃用戶評(píng)價(jià)和投訴秒殺活動(dòng)這里有個(gè)很重要的經(jīng)驗(yàn)如果時(shí)間只有兩到三個(gè)月主鏈路能跑通、能講清楚就已經(jīng)超過(guò)大部分人了。把4個(gè)模塊做深遠(yuǎn)比把10個(gè)模塊都做出來(lái)但每個(gè)都是“增刪改查”更有說(shuō)服力。1.3 把需求翻譯成頁(yè)面清單我習(xí)慣用“用戶故事”來(lái)梳理頁(yè)面。比如“消費(fèi)者小明在首頁(yè)看到今日推薦點(diǎn)擊商品詳情加入購(gòu)物車結(jié)算支付然后在訂單列表里看到物流狀態(tài)”。每個(gè)用戶故事對(duì)應(yīng)一組頁(yè)面列出來(lái)后需求邊界就非常清晰。最終頁(yè)面清單大致是消費(fèi)者端首頁(yè)、商品列表、商品詳情、購(gòu)物車、結(jié)算頁(yè)、訂單列表、訂單詳情、個(gè)人中心商戶端Dashboard、商品管理、訂單處理發(fā)貨/缺貨、銷售統(tǒng)計(jì)管理端用戶管理、商戶入駐審核、公告管理、分類管理、系統(tǒng)統(tǒng)計(jì)2. 技術(shù)選型不是越新越好夠用且能講清原理才重要2.1 選型清單和背后的理由我做這套系統(tǒng)時(shí)用的技術(shù)棧如下層面選擇說(shuō)明后端框架SpringBoot 2.7.x穩(wěn)定資料多舊版本的坑基本都被踩過(guò)了ORM框架MyBatis-Plus單表CRUD直接內(nèi)置復(fù)雜查詢手寫SQL兼顧效率與靈活數(shù)據(jù)庫(kù)MySQL 8.0用5.7也行8.0對(duì)窗口函數(shù)支持更好統(tǒng)計(jì)數(shù)據(jù)方便緩存Redis僅用于首頁(yè)緩存和購(gòu)物車優(yōu)化非必須但加上了可以顯著提升論文深度鑒權(quán)方式JWT Spring攔截器比Spring Security輕量很多也更容易解釋清楚前端Vue 3 Element Plus Axios前后端分離體現(xiàn)B/S架構(gòu)下的現(xiàn)代開(kāi)發(fā)模式構(gòu)建工具M(jìn)aven依賴管理方便團(tuán)隊(duì)配合還是單人開(kāi)發(fā)都適合當(dāng)初沒(méi)有選Spring Security是因?yàn)樗倪^(guò)濾器鏈、UserDetailsService、權(quán)限表達(dá)式這套體系對(duì)新手來(lái)說(shuō)學(xué)習(xí)曲線比較陡很容易把時(shí)間耗在配置上而不是業(yè)務(wù)上。而JWT方案的核心邏輯就三件事登錄后簽發(fā)token攔截器校驗(yàn)token從token中解析用戶身份。這個(gè)鏈路自己寫一遍之后對(duì)“無(wú)狀態(tài)鑒權(quán)”的理解會(huì)比直接用框架深得多。2.2 B/S架構(gòu)分層的直觀理解面向B/S架構(gòu)意味著用戶在瀏覽器里訪問(wèn)系統(tǒng)不需要安裝客戶端。代碼層面上一次請(qǐng)求的完整鏈路是瀏覽器Vue頁(yè)面或HTML - 發(fā)起HTTP請(qǐng)求 - Controller層接收參數(shù)、校驗(yàn)請(qǐng)求 - Service層處理業(yè)務(wù)邏輯、管理事務(wù) - Mapper層與MySQL交互 - 返回?cái)?shù)據(jù)封裝成統(tǒng)一格式 - 瀏覽器渲染展示我在實(shí)際開(kāi)發(fā)中嚴(yán)格要求自己遵守“Controller里不寫業(yè)務(wù)代碼”的原則。很多同學(xué)最終代碼的Service層很薄所有邏輯都堆在Controller里雖然功能能跑但答辯時(shí)一問(wèn)“你的業(yè)務(wù)邏輯在哪一層”就會(huì)露怯。2.3 開(kāi)發(fā)環(huán)境上兩個(gè)容易卡住的地方Maven和MySQL的版本搭配是第一個(gè)坑。Maven 3.6.3配合JDK 8是安全的組合SpringBoot 2.7.x原生支持JDK 8不需要額外配置。如果你用的是JDK 17則要注意SpringBoot必須使用2.7.0以上版本否則會(huì)報(bào)Caused by: java.lang.IllegalArgumentException之類的兼容性錯(cuò)誤。第二個(gè)坑是MySQL 8.0的驅(qū)動(dòng)變化。SpringBoot 2.7.x默認(rèn)用的是com.mysql.cj.jdbc.Driver如果你在配置文件里寫了老版的com.mysql.jdbc.Driver啟動(dòng)時(shí)會(huì)告警。最保險(xiǎn)的配置是spring.datasource.urljdbc:mysql://localhost:3306/market_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.password你的密碼 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver注意serverTimezoneAsia/Shanghai不能省略否則時(shí)間字段的查詢結(jié)果會(huì)和本地時(shí)間差8個(gè)小時(shí)。3. 數(shù)據(jù)庫(kù)設(shè)計(jì)錢和庫(kù)存是兩個(gè)最容易出錯(cuò)的地方3.1 核心表清單與關(guān)系數(shù)據(jù)庫(kù)設(shè)計(jì)是整個(gè)項(xiàng)目的根后面的代碼都是圍繞表結(jié)構(gòu)展開(kāi)的。我把核心表整理成下面這個(gè)清單表名用途重要字段user用戶表消費(fèi)者/商戶/管理員共用username、password、phone、rolemerchant商戶信息表user_id、market_name、address、audit_statuscategory商品分類表name、parent_id、sortproduct商品表name、category_id、merchant_id、price、stock、unit、image、statuscart購(gòu)物車表user_id、product_id、quantityorders訂單主表order_no、user_id、merchant_id、total_amount、status、create_timeorder_item訂單明細(xì)表order_id、product_id、product_name、price、quantity、subtotalpayment支付流水表order_id、payment_no、amount、status、pay_timeannouncement公告表title、content、create_time為什么訂單要拆成主表和明細(xì)表因?yàn)橛唵魏蜕唐肥嵌鄬?duì)多的關(guān)系。一個(gè)訂單里可能有胡蘿卜和白菜兩樣商品如果把商品名和價(jià)格直接放在訂單表里那訂單表里的字段就會(huì)冗余且難以擴(kuò)展。拆成orders order_item之后訂單主表只管總金額、狀態(tài)、下單人等核心信息明細(xì)表記錄每個(gè)商品的快照。3.2 金額為什么必須用Decimal生鮮商品的價(jià)格涉及小數(shù)點(diǎn)比如土豆2.58元/斤很多初學(xué)者喜歡用double或float來(lái)存價(jià)格這是一個(gè)非常經(jīng)典的錯(cuò)誤??纯催@段代碼double a 0.58; double b 0.01; System.out.println(a b); // 輸出 0.5899999999999999原因在于浮點(diǎn)數(shù)在計(jì)算機(jī)中是以二進(jìn)制存儲(chǔ)的很多十進(jìn)制小數(shù)無(wú)法精確表示。涉及金額的場(chǎng)景必須使用BigDecimal數(shù)據(jù)庫(kù)字段類型用DECIMAL(10, 2)。下單計(jì)算總價(jià)時(shí)也要用BigDecimal而不是先轉(zhuǎn)成double再乘。[[注意]]在JSON序列化時(shí)BigDecimal默認(rèn)會(huì)輸出成數(shù)字前端展示沒(méi)有問(wèn)題。但如果你在計(jì)算時(shí)把BigDecimal除以100之類的操作記得用divide方法時(shí)必須指定小數(shù)位數(shù)比如price.divide(new BigDecimal(100), 2, RoundingMode.HALF_UP)否則會(huì)報(bào)ArithmeticException: Non-terminating decimal expansion。3.3 庫(kù)存字段設(shè)計(jì)預(yù)留擴(kuò)展余地生鮮商品的庫(kù)存單位可能不是“件”而是“斤”“份”所以product表里我加了unit字段。庫(kù)存字段名不要用count這是SQL關(guān)鍵字最好用stock。另外哪怕你現(xiàn)在不做并發(fā)控制也要提前在表里留一個(gè)version字段后面做樂(lè)觀鎖扣庫(kù)存時(shí)可以派上大用場(chǎng)。商品表的建表SQL核心部分大致是這樣CREATE TABLE product ( id BIGINT AUTO_INCREMENT PRIMARY KEY, category_id BIGINT NOT NULL COMMENT 分類ID, merchant_id BIGINT NOT NULL COMMENT 商戶ID, name VARCHAR(100) NOT NULL COMMENT 商品名稱, price DECIMAL(10,2) NOT NULL COMMENT 單價(jià), stock INT NOT NULL DEFAULT 0 COMMENT 庫(kù)存, unit VARCHAR(20) DEFAULT 斤 COMMENT 單位, image VARCHAR(255) COMMENT 商品圖片, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, version INT DEFAULT 0 COMMENT 樂(lè)觀鎖版本號(hào), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );3.4 訂單狀態(tài)機(jī)的設(shè)計(jì)訂單狀態(tài)是整個(gè)系統(tǒng)里最容易講出彩的地方。我設(shè)計(jì)的狀態(tài)流轉(zhuǎn)是待支付(0) - 已支付(1) - 備貨中(2) - 配送中(3) - 已完成(4) 待支付(0) - 已取消(5) 已支付(1) - 申請(qǐng)退款(6) - 已退款(7)狀態(tài)用整數(shù)存并單獨(dú)建一個(gè)常量類或者枚舉類來(lái)定義千萬(wàn)不能直接在Controller里寫魔法數(shù)字。比如public class OrderStatus { public static final int PENDING_PAYMENT 0; public static final int PAID 1; public static final int PREPARING 2; public static final int DELIVERING 3; public static final int COMPLETED 4; public static final int CANCELLED 5; }這里說(shuō)一個(gè)經(jīng)驗(yàn)訂單狀態(tài)更新時(shí)SQL里一定要帶上“當(dāng)前狀態(tài)”條件。比如用戶點(diǎn)擊“取消訂單”時(shí)更新語(yǔ)句是UPDATE orders SET status 5 WHERE id #{orderId} AND status 0如果返回的影響行數(shù)是0說(shuō)明訂單已經(jīng)不是待支付狀態(tài)了這時(shí)候要提示用戶“當(dāng)前狀態(tài)不可取消”。這種寫法叫樂(lè)觀鎖在狀態(tài)流轉(zhuǎn)上的應(yīng)用能防止兩個(gè)操作同時(shí)把訂單狀態(tài)覆蓋掉。4. 核心交易鏈路代碼落地4.1 注冊(cè)登錄與JWT鑒權(quán)登錄模塊我采用的是JWT方案。用戶在登錄成功后后端用用戶的id、角色和過(guò)期時(shí)間生成一個(gè)token返回給前端。前端把它存在localStorage里之后每次請(qǐng)求在Header里帶上Authorization: Bearer xxx。JWT工具類的核心方法如下public class JwtUtil { private static final String SECRET your-secret-key-change-me; public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }攔截器里校驗(yàn)token并放行public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String authHeader request.getHeader(Authorization); if (authHeader ! null authHeader.startsWith(Bearer )) { try { Claims claims JwtUtil.parseToken(authHeader.substring(7)); request.setAttribute(userId, Long.parseLong(claims.getSubject())); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }注冊(cè)時(shí)密碼一定要加密存儲(chǔ)至少使用BCryptPasswordEncoder。千萬(wàn)不要明文存密碼這在論文里體現(xiàn)“安全性設(shè)計(jì)”時(shí)也是一個(gè)得分點(diǎn)。4.2 下單事務(wù)的邊界哪些操作要放在同一個(gè)事務(wù)里下單看起來(lái)簡(jiǎn)單但它的原子性邊界需要想清楚。我最終把以下四個(gè)操作放在一個(gè)事務(wù)方法里校驗(yàn)商品狀態(tài)和庫(kù)存是否充足生成訂單主表和訂單明細(xì)扣減商品庫(kù)存生成支付流水狀態(tài)為待支付對(duì)應(yīng)的Service方法上加Transactional注解Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto, Long userId) { // 1. 從購(gòu)物車或前端傳來(lái)的商品列表獲取items // 2. 遍歷items校驗(yàn)庫(kù)存 // 3. 計(jì)算總金額 // 4. 批量扣庫(kù)存 // 5. 插入orders和order_item // 6. 插入payment記錄 return orderVO; }rollbackFor Exception.class這一行值得專門解釋一下。默認(rèn)情況下Spring只能對(duì)RuntimeException進(jìn)行事務(wù)回滾如果你在方法里拋出了一個(gè)受檢異常比如自定義的BizException extends Exception事務(wù)是不會(huì)自動(dòng)回滾的。很多同學(xué)在這個(gè)細(xì)節(jié)上栽過(guò)跟頭我比較推薦自定義異常時(shí)直接繼承RuntimeException。4.3 扣庫(kù)存時(shí)用樂(lè)觀鎖防止超賣這是整個(gè)項(xiàng)目里最有技術(shù)含金量的點(diǎn)。在第一次測(cè)試時(shí)我用兩個(gè)賬號(hào)對(duì)同一件只剩1份的商品同時(shí)下單結(jié)果兩個(gè)訂單都成功了庫(kù)存變成了-1。原因很簡(jiǎn)單代碼里的“先查庫(kù)存再更新庫(kù)存”不是原子操作。修復(fù)方法是把庫(kù)存校驗(yàn)和扣減合并成一條SQL同時(shí)判斷庫(kù)存數(shù)量int updated productMapper.updateStock(productId, quantity);對(duì)應(yīng)的XMLupdate idupdateStock UPDATE product SET stock stock - #{quantity}, version version 1 WHERE id #{productId} AND stock #{quantity} /update如果updated 0說(shuō)明庫(kù)存不足或商品已被修改此時(shí)直接拋出“庫(kù)存不足”異常讓整個(gè)事務(wù)回滾。這里沒(méi)有用數(shù)據(jù)庫(kù)悲觀鎖SELECT ... FOR UPDATE是因?yàn)樯r商品高頻短小的查詢場(chǎng)景更多樂(lè)觀鎖在讀多寫少的畢業(yè)設(shè)計(jì)流量下完全夠用而且更容易在答辯時(shí)解釋清楚。4.4 模擬支付把接口設(shè)計(jì)成可以替換的樣子真實(shí)對(duì)接第三方支付需要商戶號(hào)、證書和回調(diào)驗(yàn)簽這在畢業(yè)設(shè)計(jì)中不太現(xiàn)實(shí)。但我采用了一種“接口隔離”的方案讓模擬支付和真實(shí)支付能夠無(wú)縫切換public interface PaymentService { PaymentResult pay(Long orderId, BigDecimal amount); } Service public class MockPaymentServiceImpl implements PaymentService { Override public PaymentResult pay(Long orderId, BigDecimal amount) { // 模擬支付固定延遲1~2秒后返回成功 // 更新訂單狀態(tài)為已支付 // 更新支付流水狀態(tài)為已支付 return PaymentResult.success(模擬支付成功); } }前端在支付頁(yè)調(diào)/api/payment/pay后端直接執(zhí)行模擬邏輯。答辯時(shí)如果你說(shuō)“這里做了一個(gè)支付接口抽象層如果接真實(shí)支付只需要新寫一個(gè)實(shí)現(xiàn)類替換即可”這個(gè)回答是很加分的。5. 真實(shí)開(kāi)發(fā)中踩過(guò)的坑5.1 前端跨域和統(tǒng)一返回格式前后端分離之后本地開(kāi)發(fā)時(shí)Vue在8080端口后端在8081端口瀏覽器默認(rèn)會(huì)攔截跨域請(qǐng)求。最初我在Controller里一個(gè)一個(gè)寫CrossOrigin非常丑陋也不規(guī)范。后來(lái)配置了一個(gè)WebMvcConfigurer全局跨域Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }同時(shí)我封裝了一個(gè)統(tǒng)一返回格式ResultT包含code、message、data三個(gè)字段。成功返回Result.success(data)失敗返回Result.error(參數(shù)錯(cuò)誤)。這樣前端只需要攔截一次響應(yīng)根據(jù)code做統(tǒng)一處理而不是每個(gè)接口各自為政。5.2 MyBatis-Plus的分頁(yè)插件沒(méi)配置就會(huì)導(dǎo)致分頁(yè)失效使用MyBatis-Plus分頁(yè)時(shí)selectPage()方法如果不配置分頁(yè)插件你會(huì)發(fā)現(xiàn)它對(duì)傳入的Page參數(shù)無(wú)動(dòng)于衷直接查出全表。這是因?yàn)榉猪?yè)功能依賴MybatisPlusInterceptor里注冊(cè)的PaginationInnerInterceptorConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }這個(gè)坑的癥狀非常隱蔽頁(yè)面能出數(shù)據(jù)但數(shù)據(jù)庫(kù)里明明有50條記錄第一頁(yè)卻顯示了全部。排查辦法是看控制臺(tái)打出的SQL有沒(méi)有LIMIT關(guān)鍵字。5.3 前端時(shí)間字段顯示差8小時(shí)訂單創(chuàng)建時(shí)間傳給前端后頁(yè)面上顯示的時(shí)間比實(shí)際時(shí)間少了8個(gè)小時(shí)。原因有兩個(gè)一是MySQL連接串沒(méi)有加serverTimezoneAsia/Shanghai前面提過(guò)二是Jackson序列化LocalDateTime時(shí)默認(rèn)采用UTC時(shí)區(qū)。解決方式是在application.yml里明確序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8實(shí)體類里時(shí)間字段習(xí)慣上用LocalDateTime而不是Date配合上面的配置后前后端顯示時(shí)間就一致了。5.4 文件上傳圖片路徑到底存什么商品圖片上傳時(shí)我犯過(guò)一個(gè)錯(cuò)誤直接把上傳后的文件完整路徑比如D:/upload/xxx.jpg存進(jìn)了數(shù)據(jù)庫(kù)。這個(gè)路徑在本地能用但換一臺(tái)機(jī)器部署就失效而且前端瀏覽器也不會(huì)允許直接訪問(wèn)本地的D盤路徑。正確的做法是把文件上傳到項(xiàng)目的靜態(tài)資源目錄下比如/resources/upload/然后用http://localhost:8081/upload/xxx.jpg這樣的相對(duì)URL風(fēng)格訪問(wèn)。配置一個(gè)靜態(tài)資源映射registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /);數(shù)據(jù)庫(kù)里只存/upload/xxx.jpg前端通過(guò)后端域名拼接出完整的圖片地址。這樣項(xiàng)目換服務(wù)器后只要上傳目錄路徑改一下配置圖片就能繼續(xù)訪問(wèn)。5.5 統(tǒng)計(jì)報(bào)表里的小數(shù)計(jì)算管理端的數(shù)據(jù)統(tǒng)計(jì)功能里銷售額需要按月匯總。如果在SQL里用SUM(order_item.subtotal)直接計(jì)算由于subtotal是Decimal類型結(jié)果沒(méi)問(wèn)題。但如果你把Decimal轉(zhuǎn)成Float去計(jì)算匯總就會(huì)碰到浮點(diǎn)數(shù)精度問(wèn)題。我最終在SQL層面全部使用ROUND(SUM(amount), 2)來(lái)處理Java代碼中統(tǒng)計(jì)結(jié)果的DTO字段類型也用String或BigDecimal來(lái)承接避免JSON序列化帶來(lái)的精度丟失。6. 答辯準(zhǔn)備和論文結(jié)構(gòu)怎么搭6.1 做一份演示腳本答辯時(shí)最尷尬的事不是bug崩潰而是演示到一半不知道下一步要操作什么。我建議提前準(zhǔn)備一份演示腳本按正常用戶操作路徑走一遍注冊(cè)一個(gè)新用戶登錄在首頁(yè)搜索“土豆”加入購(gòu)物車購(gòu)物車?yán)镎{(diào)整數(shù)量結(jié)算下單訂單列表看到待支付訂單執(zhí)行模擬支付切換商戶賬號(hào)在訂單管理里看到新訂單執(zhí)行發(fā)貨切回消費(fèi)者賬號(hào)看到訂單狀態(tài)變?yōu)榕渌椭星械焦芾韱T賬號(hào)查看銷售統(tǒng)計(jì)確認(rèn)金額正確演示時(shí)不要點(diǎn)開(kāi)那種需要輸入復(fù)雜數(shù)據(jù)的表單盡量用提前構(gòu)造好的演示賬號(hào)和數(shù)據(jù)。也可以準(zhǔn)備一個(gè)小號(hào)專門用來(lái)演示登錄注冊(cè)流程。6.2 論文的核心章節(jié)怎么寫雖然每個(gè)學(xué)校的論文模板不同但大體邏輯是一樣的。我的論文目錄大致如下第一章 引言背景、目的與意義、國(guó)內(nèi)外現(xiàn)狀第二章 相關(guān)技術(shù)介紹SpringBoot、MyBatis-Plus、Vue、Redis、JWT第三章 系統(tǒng)分析可行性分析、需求分析、用例分析第四章 系統(tǒng)設(shè)計(jì)總體架構(gòu)、功能模塊設(shè)計(jì)、數(shù)據(jù)庫(kù)設(shè)計(jì)第五章 系統(tǒng)實(shí)現(xiàn)每個(gè)功能模塊的實(shí)現(xiàn)和核心代碼第六章 系統(tǒng)測(cè)試測(cè)試用例、測(cè)試結(jié)果、性能分析第七章 總結(jié)與展望關(guān)于“國(guó)內(nèi)外現(xiàn)狀”這一章很多同學(xué)寫不好大段抄別人的話。我的建議是聚焦“農(nóng)產(chǎn)品電商在國(guó)內(nèi)的生鮮配送模式演進(jìn)”比如從傳統(tǒng)B2C到前置倉(cāng)到社區(qū)團(tuán)購(gòu)然后落腳在“如何在校園/農(nóng)貿(mào)市場(chǎng)場(chǎng)景設(shè)計(jì)輕量級(jí)交易系統(tǒng)”這個(gè)定位就很自然順暢。6.3 常見(jiàn)答辯問(wèn)題清單根據(jù)我的經(jīng)驗(yàn)導(dǎo)師最喜歡問(wèn)的問(wèn)題集中在以下幾個(gè)方向?yàn)槭裁催x擇SpringBoot而不是SSM——回答框架演進(jìn)、約定優(yōu)于配置、生態(tài)整合訂單超賣問(wèn)題是怎么解決的——樂(lè)觀鎖SQL更新庫(kù)存、事務(wù)回滾用戶密碼是怎么存儲(chǔ)的——BCrypt加密不存明文如果樓上說(shuō)的模擬支付換成真實(shí)微信支付需要改動(dòng)哪些地方——增加微信支付實(shí)現(xiàn)類配置回調(diào)接口驗(yàn)簽等系統(tǒng)安全性怎么體現(xiàn)——JWT身份認(rèn)證、權(quán)限攔截器、統(tǒng)一異常處理、密碼加密這些問(wèn)題要提前用自己的語(yǔ)言組織好答案特別是“樂(lè)觀鎖”那段需要能把SQL和事務(wù)講清楚最好配合代碼示意。6.4 時(shí)間安排建議我做這個(gè)項(xiàng)目整體用時(shí)大約兩個(gè)月主要時(shí)間分配是階段時(shí)間內(nèi)容需求分析技術(shù)預(yù)演第一周確定功能列表、搭建項(xiàng)目骨架、跑通HelloWorld數(shù)據(jù)庫(kù)設(shè)計(jì)與后端基礎(chǔ)CRUD第二周至第三周表結(jié)構(gòu)、用戶模塊、商品模塊核心交易鏈路第四周至第五周購(gòu)物車、下單、支付、訂單狀態(tài)流轉(zhuǎn)管理端功能第六周商品審核、數(shù)據(jù)統(tǒng)計(jì)、公告管理前端整合與聯(lián)調(diào)第七周Vue頁(yè)面完善、接口聯(lián)調(diào)、修bug論文撰寫與答辯準(zhǔn)備第八周按學(xué)校模板整理內(nèi)容、測(cè)試、答辯PPT如果你的基礎(chǔ)比較薄弱我建議把技術(shù)預(yù)演的時(shí)間拉長(zhǎng)一點(diǎn)先單獨(dú)做一個(gè)涉及“兩個(gè)表聯(lián)合查詢新增數(shù)據(jù)”的小Demo確認(rèn)自己對(duì)SpringBoot的請(qǐng)求流轉(zhuǎn)和MyBatis-Plus操作熟練后再開(kāi)始正式開(kāi)發(fā)否則很容易在項(xiàng)目初期就陷入“到處搜報(bào)錯(cuò)”的泥潭。最后再說(shuō)幾個(gè)我親測(cè)有效的小建議第一一定要用Git從第一天就開(kāi)始管理代碼哪怕只有你一個(gè)人開(kāi)發(fā)。每完成一個(gè)模塊就提交一次版本寫論文時(shí)按提交記錄來(lái)回憶開(kāi)發(fā)過(guò)程比翻聊天記錄和桌面文件高效得多。第二開(kāi)發(fā)過(guò)程中把遇到的異常和解決方案記錄在一個(gè)筆記文件里這部分內(nèi)容可以直接轉(zhuǎn)化成論文中的“系統(tǒng)調(diào)試與問(wèn)題分析”小節(jié)也是答辯時(shí)展示你真實(shí)做過(guò)項(xiàng)目的證據(jù)。第三系統(tǒng)的命名和注釋盡量規(guī)范類名用ProductController而不是productController注釋寫清楚業(yè)務(wù)意圖因?yàn)檎撐睦飼?huì)直接放核心代碼截圖代碼質(zhì)量會(huì)直接影響老師對(duì)論文的印象。農(nóng)貿(mào)市場(chǎng)管理系統(tǒng)和農(nóng)產(chǎn)品在線交易平臺(tái)這個(gè)題目看似樸素但它把B/S架構(gòu)下最典型的業(yè)務(wù)場(chǎng)景完整覆蓋了一遍。做扎實(shí)之后你收獲的不僅僅是一份能通過(guò)答辯的項(xiàng)目而是對(duì)SpringBoot開(kāi)發(fā)流程和電商業(yè)務(wù)邏輯的整體感知。如果你正在考慮類似的題目希望這篇復(fù)盤能幫你減少一些摸索的時(shí)間。