實戰(zhàn))
校園里的閑置交易做過的人都知道有多麻煩。去年我在某高校做畢設調(diào)研時發(fā)現(xiàn)幾乎所有學生群都有人在賣舊書、臺燈、自行車但信息幾分鐘就被聊天刷沒了想找同款基本靠緣分。當時就決定做一個專門的校園二手交易平臺技術(shù)棧選了 Spring Boot 加微信小程序也就是標題里那個“springboot校園二手交易平臺小程序”。這篇內(nèi)容不打算只貼源碼和截圖而是把整個項目的需求拆解、數(shù)據(jù)庫設計、核心接口邏輯、小程序端實現(xiàn)以及我最想分享的踩坑記錄都寫出來。哪怕你不想關(guān)注拿源碼單按這篇文章里的架構(gòu)思路和數(shù)據(jù)表去復刻也能把一個可上線運行的二手交易系統(tǒng)搭起來。這個項目適合誰正在做畢業(yè)設計的學生、想系統(tǒng)走一遍前后端分離開發(fā)的初學者、或者單純想了解校園交易類小程序業(yè)務閉環(huán)的產(chǎn)品和技術(shù)新人。閱讀過程中你會發(fā)現(xiàn)它沒有懸乎的高并發(fā)和分布式核心價值在于把“登錄、發(fā)帖、交易、狀態(tài)流轉(zhuǎn)”這幾件小事做扎實而這恰恰是很多二手平臺項目翻車的地方。1. 項目從需求到架構(gòu)先搞清楚二手交易到底在交易什么1.1 校園閑置場景的真實痛點校園二手交易和閑魚同城交易不太一樣。閑魚的買家賣家是完全陌生關(guān)系靠的是芝麻信用、平臺擔保和事后評價體系來建立信任而校園場景里買賣雙方大概率都在同一個校區(qū)、同一棟宿舍樓甚至可能是同一個學院的信任基礎天然存在但信息和撮合機制卻很原始。我在前期調(diào)研里梳理出幾個典型問題第一信息承載分散社團群、表白墻、宿舍群各有各的帖子沒有統(tǒng)一的瀏覽和搜索入口第二商品的成色、價格、交易方式完全靠賣家自己描述買家需要反復私聊確認溝通成本高第三缺少訂單狀態(tài)概念說好面交卻放鴿子的情況非常多第四沒有“已售出”的標記過期很久的商品還在被點贊和詢問體驗很差。因此這個項目的核心需求不是做一個“能登錄、能發(fā)帖”的玩具而是要完成一條相對完整的交易閉環(huán)用戶登錄 - 發(fā)布商品 - 買家瀏覽/搜索/聯(lián)系 - 表達購買意向 - 雙方確認 - 訂單狀態(tài)流轉(zhuǎn) - 線下交易完成。再加上收藏、留言、個人中心等輔助功能就是一個能自洽的業(yè)務模型。1.2 為什么是 Spring Boot 加微信小程序技術(shù)選型上后端我最終定了 Spring Boot前端定了微信原生小程序沒有引入太重的微服務或跨端框架。理由很直接。Spring Boot 的優(yōu)勢在于快速搭建、生態(tài)成熟。這個系統(tǒng)需要的能力無非是 REST API、文件存儲、鑒權(quán)、數(shù)據(jù)庫訪問Spring Boot 都能用很簡潔的方式集成而且網(wǎng)上資料多遇到問題能找到大量參考。相比 Go、Node 之類的方案對剛接觸全棧開發(fā)的人來說Spring Boot 的思維方式更規(guī)整分層也更清晰。微信小程序作為前端載體則是因為它天然貼合校園場景。學生不需要額外下載 App微信掃一下就進去分享到群和朋友圈也方便。小程序的原生框架對后端的請求封裝、登錄態(tài)管理、圖片上傳都有現(xiàn)成接口開發(fā)成本比 React Native 或 Flutter 低不少。而且小程序的發(fā)布審核并不會卡常規(guī)分類的二手交易類目只要避免出現(xiàn)明顯的平臺化交易嫌疑比如不要內(nèi)置支付通道審核就不會有太大問題。整套系統(tǒng)采用前后端分離小程序端只負責界面交互和請求發(fā)送后端只負責業(yè)務邏輯、數(shù)據(jù)校驗和接口響應。這樣做的直接好處是后續(xù)如果要加一個管理后臺或者把小程序端換成 H5完全不用動后端核心邏輯。1.3 模塊拆分與數(shù)據(jù)流方向在畫 ER 圖之前我先按業(yè)務角色把系統(tǒng)拆成了幾個模塊用戶模塊、商品模塊、訂單模塊、收藏與留言模塊、圖片存儲模塊。每個模塊的職責盡量單一模塊之間只通過接口或者數(shù)據(jù)表外鍵產(chǎn)生關(guān)聯(lián)。數(shù)據(jù)流的方向是這樣的小程序端向 Spring Boot 后端發(fā)起 HTTP 請求后端通過攔截器統(tǒng)一處理 Token 鑒權(quán)Controller 層接收參數(shù)并做基礎校驗Service 層處理業(yè)務邏輯Mapper 層通過 MyBatis 與 MySQL 交互。圖片上傳走獨立的接口先落盤到服務器存儲目錄再返回 URL。所有接口統(tǒng)一返回 JSON格式是{ code, message, data }小程序端對這個結(jié)構(gòu)做統(tǒng)一解析和處理。這個數(shù)據(jù)流聽起來簡單但實際操作中如果不提前把統(tǒng)一返回結(jié)構(gòu)定死前后端聯(lián)調(diào)時會有無窮無盡的體力消耗。我強烈建議在項目一開始就定好Result包裝類和錯誤碼規(guī)范避免一個接口返回true、另一個接口返回success聯(lián)調(diào)時全靠猜。2. 后端核心實現(xiàn)表結(jié)構(gòu)、登錄態(tài)與交易狀態(tài)機2.1 數(shù)據(jù)庫設計要把業(yè)務演變想清楚數(shù)據(jù)庫是這種業(yè)務系統(tǒng)的地基表結(jié)構(gòu)設計得不好后面寫接口會到處別扭。我最終設計的核心表如下user用戶表字段包括id、openid、nickname、avatar_url、phone、campus、create_time。product商品表id、user_id、title、description、price、original_price、condition_level、category_id、status、view_count、create_time、update_time。product_image商品圖片表id、product_id、image_url、sort_order。category分類表id、name、icon。order訂單表id、order_no、product_id、buyer_id、seller_id、amount、status、create_time、update_time。favorite收藏表id、user_id、product_id、create_time。message留言表id、product_id、from_user_id、to_user_id、content、create_time。這里有一個關(guān)鍵點product表里的status字段我用了0-在售、1-已售出、2-下架而不是直接刪除商品。二手平臺最忌諱真正刪除商品記錄因為訂單和留言都外鏈到商品一旦刪了歷史記錄就全斷了管理后臺也無法追溯。用戶“刪除”自己的商品時實際執(zhí)行的是軟刪除也就是只改狀態(tài)不刪行。價格字段要特別注意。單價用DECIMAL(10, 2)而不是FLOAT因為浮點數(shù)在計算時會有精度誤差訂單金額、廣告位價格這類數(shù)據(jù)一旦出錯就是事故。我見過有人用浮點存價格導致對賬少了幾塊錢的例子校園項目雖然量小但該嚴謹?shù)牡胤讲荒芩?。索引方面product表的status和create_time要建聯(lián)合索引因為首頁列表、分類商品列表基本都按“在售并且時間倒序”來查。order表的buyer_id和seller_id分別建索引用于個人中心的“我買到的/我賣出的”列表查詢。如果數(shù)據(jù)量到幾十萬級別這些索引會極大緩解慢查詢不過現(xiàn)階段規(guī)模下沒必要做分庫分表。2.2 微信登錄與 Token 鑒權(quán)的完整鏈路微信小程序的登錄是典型的 code2session 換 openid 流程很多新手在這里被繞暈。我整理一下實際代碼里的順序小程序端調(diào)用wx.login()拿到臨時憑證code。小程序把code發(fā)給后端/api/auth/login。后端拿著code調(diào)用微信接口code2session換取openid和session_key。后端根據(jù)openid查詢用戶如果沒有則自動注冊一個新用戶并生成默認昵稱和頭像。后端為用戶簽發(fā)一個自定義的 Token返回給小程序小程序存入wx.setStorageSync后續(xù)每個請求的頭里帶上Authorization。Token 方案我選擇的是自定義 Token 存 Redis而不是 JWT。原因很樸素JWT 是無狀態(tài)的用戶被封禁或改頭像之后已經(jīng)簽發(fā)的 Token 仍然有效想踢人下線只能等過期。校園平臺雖然不需要太強管控但如果以后要做管理員封號功能存 Redis 的 Token 能實現(xiàn)即時失效靈活得多。后端用攔截器統(tǒng)一處理 Token 解析。攔截器里從請求頭拿出Authorization從 Redis 查用戶信息查到了就往ThreadLocal里塞當前用戶 IDController 層就可以直接用注解拿到當前登錄用戶。查不到就返回401。注意code2session的接口有每天調(diào)用上限雖然日常項目不會觸發(fā)但一定不要把code寫在日志里。這個臨時憑證和session_key一旦泄露存在被偽造請求的風險。2.3 商品發(fā)布、圖片上傳與列表查詢商品發(fā)布接口是核心中的核心邏輯上分兩部分先處理圖片再寫入商品信息。圖片上傳我用的方式是小程序端wx.chooseMedia選擇圖片通過wx.uploadFile以multipart/form-data形式傳到后端/api/upload。后端接收文件后先校驗文件大小、擴展名只允許 jpg/png/webp單張不超過 5MB然后按日期存儲到本地目錄生成帶時間戳的隨機文件名避免重名覆蓋。文件名我習慣用yyyyMMddHHmmss 隨機數(shù)的格式比如20250605153021_8291a.jpg這樣即使用戶用一個叫“1.jpg”的文件反復上傳也不會撞車。上傳成功后后端返回 URL 列表小程序端再把 URL 和商品表單內(nèi)容一起提交到發(fā)布接口。這里有一個常見的坑有些開發(fā)者會在上傳接口里順便創(chuàng)建商品記錄結(jié)果用戶傳了圖但沒填標題就離開了數(shù)據(jù)庫里就會多出一堆臟數(shù)據(jù)。正確做法是上傳僅返回地址發(fā)布接口才真正落庫商品表里維護一個product_image子表來關(guān)聯(lián)多個圖片 URL。商品列表查詢要考慮過濾條件按分類、按價格區(qū)間、按關(guān)鍵詞搜索。關(guān)鍵詞搜索我用的是LIKE %keyword%匹配標題和描述數(shù)據(jù)量小時完全夠用。如果以后商品量暴增再升級到全文檢索也不遲。2.4 訂單狀態(tài)機讓買賣雙方都有明確預期二手交易系統(tǒng)的訂單狀態(tài)不能像電商那樣復雜但也不能完全沒有。我設計了五個狀態(tài)待付款、待發(fā)貨、待收貨、已完成、已取消。當然校園二手很大程度是線下當面交易所以“待發(fā)貨”和“待收貨”在實際場景里可以解釋成“約定時間地點”和“線下交易完成”。訂單的生成要防重復。同一商品同一時間只能有一個買家發(fā)起交易后端在生成訂單前必須查一次商品狀態(tài)并且用數(shù)據(jù)庫的樂觀鎖或UPDATE ... WHERE status 在售的原子更新來搶占商品。我最終選了UPDATE product SET status 1 WHERE id ? AND status 0這種方式如果更新影響行數(shù)為 0說明商品已經(jīng)被別人搶占了直接返回“商品已售出”。這個設計在并發(fā)量不高時完全夠用而且避免了先查再更新的時間差問題。我實測過用 Postman 并發(fā)打 20 個同樣的購買請求只有 1 個成功其余全部正確攔截。如果將來并發(fā)量變大可以換成 Redis 分布式鎖但目前沒必要過度設計。確認收貨之后買賣雙方都可以看到訂單的完成狀態(tài)。如果有一方超過約定時間沒有操作可以在列表里提供“提醒對方”的功能通過站內(nèi)信或模板消息觸發(fā)。注意這里我沒有接入真實支付和自動發(fā)貨邏輯因為校園二手以面交為主接入支付反而會把項目變得復雜且審核風險升高。2.5 留言與收藏增強購買意向的表達留言功能本質(zhì)是提供一個輕量的商品溝通渠道。它的字段只有product_id、from_user_id、to_user_id、content、create_time。查詢時按商品ID倒序取最新 20 條不需要做網(wǎng)絡通信因為校內(nèi)當面溝通更高效。這里要注意防止 XSS用戶輸入的留言內(nèi)容在存儲和展示前都要做、、、、等字符的轉(zhuǎn)義處理。收藏功能則是給買家一個“先存下來再慢慢考慮”的入口。收藏表用user_id product_id做聯(lián)合唯一索引確保同一個人不能重復收藏同一商品。收藏數(shù)量不用實時統(tǒng)計到商品表可以在需要時COUNT(*)查出來展示。這也是性能和簡潔之間的平衡。3. 小程序端頁面設計、請求封裝與表單校驗3.1 整體頁面結(jié)構(gòu)與導航設計小程序端我采用了五個 Tab 的結(jié)構(gòu)首頁、分類、發(fā)布、消息、我的。這個結(jié)構(gòu)對二手交易類應用來說比較成熟用戶進入后第一眼看到的是商品流發(fā)布按鈕固定在顯眼位置消息中心承載留言和訂單通知我的頁面展示個人數(shù)據(jù)和功能入口。首頁商品流用雙列瀑布流展示在售商品每張卡片包含主圖、標題、價格、成色標簽。瀑布流要注意圖片比例問題不同賣家上傳的圖片比例可能不同我在后端返回圖片URL時額外給每個URL附加了寬高比字段小程序端用modeaspectFill統(tǒng)一裁切視覺上整齊很多。很多新手忽略這個細節(jié)結(jié)果首頁有的卡片高有的卡片矮顯得很凌亂。分類頁則按預設的分類展示商品左側(cè)分類欄、右側(cè)商品列表點擊分類后請求對應接口并置頂刷新。分類數(shù)據(jù)來自數(shù)據(jù)庫不寫死在前端這樣以后加分類不用發(fā)新版小程序。3.2 發(fā)布功能的交互與校驗發(fā)布頁是體現(xiàn)項目完成度的試金石。我參考閑魚的發(fā)布交互頂部選擇商品分類中間是標題輸入、描述輸入、價格輸入、成色選擇下面是九宮格圖片上傳。圖片上傳區(qū)域在用戶選擇圖片后立即上傳到服務器但商品本身要等用戶點擊“發(fā)布”才創(chuàng)建。表單校驗要前置到前端。標題必填且限制 20 字以內(nèi)價格必須為正數(shù)且最多兩位小數(shù)成色必須選擇一種。前端校驗過了后端接口還會再校驗一遍雙重校驗可以避免繞過客戶端直接調(diào)接口產(chǎn)生的臟數(shù)據(jù)。價格輸入的實現(xiàn)上我用typedigit讓鍵盤直接彈出數(shù)字鍵盤再配合maxlength6限制輸入長度。發(fā)布成功的提示我用了一個輕量 toast然后跳轉(zhuǎn)到“我的發(fā)布”列表用戶能立刻看到自己剛發(fā)布的商品。如果跳回首頁因為列表頁沒有刷新的概念用戶會以為發(fā)布失敗了這是產(chǎn)品交互上的細節(jié)影響體驗非常大。3.3 請求統(tǒng)一封裝與登錄態(tài)管理小程序端最需要規(guī)劃的就是網(wǎng)絡請求。我在utils/request.js里統(tǒng)一封裝了wx.request每次請求前自動從本地存儲讀取 Token放進Authorization請求頭每次響應先判斷 HTTP 狀態(tài)碼再判斷業(yè)務碼code如果業(yè)務碼是401就自動跳轉(zhuǎn)登錄頁。這個封裝讓業(yè)務代碼里完全不需要寫wx.request的細節(jié)頁面里只要寫request({ url: /api/product/list, method: GET })就能拿到數(shù)據(jù)?;卣{(diào)地獄的問題也通過 Promise 解決了異步邏輯用async/await寫起來非常順。注意小程序真機調(diào)試時一定要在微信公眾平臺配置 request 合法域名并且域名必須備案且支持 HTTPS。開發(fā)階段可以在開發(fā)者工具里勾選“不校驗合法域名”但上線前必須改回來否則正式版會白屏。這個坑我踩過一次排查了很久才意識到是域名沒有加進白名單。3.4 商品詳情、訂單流程與消息提醒商品詳情頁除了展示圖片和價格還要展示賣家的信息比如昵稱、頭像、校區(qū)以及“聯(lián)系買家”和“立即購買”兩個操作按鈕。聯(lián)系買家走的是留言或引導復制微信號考慮到項目審核我沒有做內(nèi)置聊天而是用留言板加復制聯(lián)系方式的組合。訂單流程在小程序端的表現(xiàn)是買家點擊“立即購買”后彈窗確認價格確認后調(diào)用后端下單接口賣家在“我賣出的”列表里看到新訂單可以點擊“約定的線下交易已完成”訂單進入待收貨狀態(tài)買家收到貨后確認訂單完成。每一步操作后端都會更新狀態(tài)并校驗當前用戶是否是訂單的買方或賣方防止越權(quán)操作。消息中心展示兩類內(nèi)容最新留言和訂單狀態(tài)變更。數(shù)據(jù)來自后端聚合查詢將留言記錄和訂單狀態(tài)變更記錄合并按時間排序。這里要注意分頁我用了簡單的“下拉加載下一頁”模式每頁 10 條避免一次性拉取太多數(shù)據(jù)導致頁面卡頓。4. 部署上線與問題排查把項目真正跑起來4.1 從本地到服務器的部署流程這個項目是標準的單體 Spring Boot 應用部署不復雜但步驟要踩對。我用的方案是Maven 打包成可執(zhí)行的 jar通過 scp 上傳到服務器再用nohup java -jar啟動配合 Nginx 做反向代理和 HTTPS 終結(jié)。打包時要注意 profile 的切換。開發(fā)環(huán)境連本地 MySQL生產(chǎn)環(huán)境要連服務器上的 MySQL數(shù)據(jù)庫地址和密碼不能寫死。我的做法是 Spring Boot 的多環(huán)境配置application-dev.yml和application-prod.yml打包時通過--spring.profiles.activeprod指定環(huán)境。不然每次部署都要改配置文件改漏一次就等著聯(lián)調(diào)翻車。Nginx 部分要配置兩個轉(zhuǎn)發(fā)一是把/api/開頭的請求轉(zhuǎn)發(fā)到 Spring Boot 的8080端口二是把靜態(tài)圖片目錄映射到一個對外可訪問的 URL比如http://你的域名/upload/指向服務器上的上傳目錄。HTTPS 證書用免費的證書服務配置好 SSL 后微信小程序端才能正常發(fā)起請求。4.2 常見問題與排查速查表這一部分是我實際開發(fā)中遇到的典型問題整理成表格方便快速定位現(xiàn)象可能原因解決方案小程序請求全部失敗真機上白屏request 合法域名未配置或域名未備案在微信公眾平臺配置合法域名使用 HTTPS登錄后用戶信息不顯示Token 解析失敗或 Redis 中 Key 過期檢查攔截器邏輯確認 Redis 是否有數(shù)據(jù)Token 有效期設為 7 天圖片上傳成功但商品頁圖片裂開圖片 URL 用了相對路徑小程序無法訪問返回絕對 URL或在前端拼接完整域名同一個商品被兩個人同時下單成功缺少商品狀態(tài)原子更新使用UPDATE product SET status 1 WHERE id ? AND status 0發(fā)布商品列表偶發(fā)重復數(shù)據(jù)頁面下拉刷新與上拉加載事件沖突用 loading 鎖防止并發(fā)請求價格出現(xiàn) 0.30000000000000004數(shù)據(jù)庫或 JSON 用了浮點數(shù)數(shù)據(jù)庫用 DECIMALJava 用 BigDecimal4.3 安全和性能的幾個硬性原則安全方面有幾條我始終堅持的底線。第一是越權(quán)防護查詢訂單和操作商品前必須校驗當前登錄用戶與資源所有者是否為同一人不能只依賴前端隱藏按鈕接口層面必須做攔截。第二是參數(shù)校驗不僅依賴前端后端 Controller 層要用Valid或手動校驗所有關(guān)鍵參數(shù)包括分頁的page、size不能為負排序字段用白名單。第三是文件上傳的安全圖片類型要校驗真實文件頭而不是只看擴展名惡意腳本文件不能進服務器。性能方面現(xiàn)階段業(yè)務量不大最值得關(guān)注的是 SQL 查詢效率和圖片體積。列表查詢不要SELECT *只需要返回卡片需要的字段圖片上傳前在小程序端先壓縮一遍可以極大減少存儲空間和加載帶寬。我在項目里用wx.compressImage把圖片統(tǒng)一壓到最大寬度 800 像素保證清晰度的同時體積控制在 200KB 以內(nèi)首頁加載速度明顯更快。5. 做完整個項目的體會與后續(xù)擴展思路5.1 幾個親測有效的開發(fā)心得這個項目完整走下來我最大的體會是業(yè)務流程的完整度比技術(shù)花活重要得多。很多二手交易平臺的項目案例只做了商品 CRUD根本沒有訂單狀態(tài)、沒有交易沖突處理、沒有越權(quán)校驗看起來功能挺多但本質(zhì)是管理系統(tǒng)而不是交易系統(tǒng)。做這類項目先畫狀態(tài)圖先定義好訂單的生命周期再動代碼能省掉后期大量返工。第二個體會是日志和聯(lián)調(diào)工具要早用早熟。我在開發(fā)階段就養(yǎng)成了每次接口調(diào)用都打印請求參數(shù)和響應結(jié)果的習慣而且用 Postman 或者 Apifox 管理接口文檔聯(lián)調(diào)時雙方都對著同一份文檔爭論少很多。如果你自己同時負責前后端也要注意別因為“自己心里有數(shù)”就不寫接口文檔兩周后再看自己都容易遺忘字段含義。第三小程序端的錯誤提示要友好。500 錯誤時不能直接彈“服務器內(nèi)部錯誤”這種摸不著頭腦的話。我封裝請求時對code做了映射例如“商品已售出”對應提示“手慢了一步商品已經(jīng)被買走了”“401”對應“登錄已過期請重新進入”。好的錯誤提示能把用戶從困惑里拉回來不怎么增加開發(fā)成本但體驗提升非常明顯。5.2 這個項目還能往哪些方向擴展如果這個項目要繼續(xù)迭代我最建議加一個管理后臺因為運營校園二手平臺總得有人處理違規(guī)商品和用戶糾紛。管理后臺可以復用后端的接口單獨做一套 Web 前端用 Vue 或 React 都行核心功能包括商品審核、用戶管理、訂單數(shù)據(jù)看板。第二個優(yōu)先級是對消息通知的完善?,F(xiàn)在消息中心的實現(xiàn)是被動拉取用戶離開小程序后就不知道自己的商品有沒有被留言。接入微信的訂閱消息后可以在被留言、訂單狀態(tài)變更時推送模板消息用戶留存率會高很多。這里注意訂閱消息的模板要提前申請而且推送是一次性的需要在用戶觸發(fā)操作時向后端傳遞授權(quán)結(jié)果。再往后可以考慮做價格參考和信用體系。價格參考可以從歷史成交記錄里算同品類均價幫買賣雙方定一個公平價格信用體系可以來自用戶完成訂單數(shù)量和實名認證不用太復雜一個信譽分字段加上最近 30 天成交記錄就夠了。這些擴展不會改變核心交易鏈路但能把系統(tǒng)的可用性往上拉一個臺階。5.3 最后分享一點我踩過的坑最后說一個我印象最深的坑開發(fā)初期我直接用了懶加載的圖片組件wx.lazy-load結(jié)果首頁瀑布流的圖片在快速滑動時經(jīng)常出現(xiàn)空白占位用戶以為頁面出 bug 了。調(diào)試了很久發(fā)現(xiàn)是圖片 URL 不帶寬高信息組件無法預知尺寸。我最終的處理是后端返回圖片時順帶返回寬高比前端拿到比例后立即布局再配合懶加載這個問題就消失了。類似這樣的細節(jié)還有很多比如訂單號不能只靠時間戳、發(fā)布按鈕在 Tab 中間不能是普通頁面跳轉(zhuǎn)、退出登錄必須清掉本地存儲和緩存。這些都是文檔里不會寫、但真要做產(chǎn)品時躲不過去的細節(jié)。希望這篇內(nèi)容能幫你少走幾步彎路把精力花在真正有意義的功能和體驗上。