實戰(zhàn):從后端搭建到小程序聯(lián)調(diào))
簡介這份資源是基于SSM框架的微信小程序房屋租賃系統(tǒng)完整項目源碼面向計算機相關(guān)專業(yè)的畢業(yè)設(shè)計、課程設(shè)計學(xué)生以及Java Web初學(xué)者。項目將Spring、SpringMVC、MyBatis與微信小程序結(jié)合覆蓋房源發(fā)布、租房需求、在線預(yù)約看房、支付與訂單管理等核心業(yè)務(wù)幫助讀者理解前后端分離的輕量級應(yīng)用開發(fā)流程。壓縮包共1343個文件約4.05MB包含59個Java源文件、53個JSP頁面、30個WXSS與29個WXML小程序頁面以及大量HTML、JS、CSS、PNG等前端資源另有1個SQL數(shù)據(jù)庫腳本和若干配置文件結(jié)構(gòu)清晰、模塊分明。已有48人學(xué)習(xí)下載。讀者可從中獲得完整的賽題級實現(xiàn)方案包括控制器分層設(shè)計、數(shù)據(jù)庫表結(jié)構(gòu)、小程序頁面布局與接口調(diào)用思路適合直接參考或二次開發(fā)快速搭建自己的房屋租賃系統(tǒng)。1. 從一份 SSM 微信小程序房屋租賃系統(tǒng)說起誰需要它能解決什么如果你手上有「基于SSM的微信小程序房屋租賃系統(tǒng)設(shè)計.zip」這類標(biāo)題的項目大概率是三種人之一正在做畢設(shè)的學(xué)生、想快速搭一套房源管理后臺的獨立開發(fā)者、或者被中介業(yè)務(wù)方催著要一個「能看房、能預(yù)約、能管房源」的小團隊。這個標(biāo)題拆開看其實就三塊SSMSpring SpringMVC MyBatis做后端微信小程序做用戶端房屋租賃系統(tǒng)是業(yè)務(wù)場景。它要解決的核心問題很樸素——房東或運營方在后臺錄房源租客在小程序里按區(qū)域、租金、戶型篩選點進(jìn)去看詳情、預(yù)約看房、提交租賃意向管理員再在后臺處理這些線索。我見過太多人一上來就糾結(jié)「用不用 SpringBoot」其實 SSM 和 SpringBoot 不是對立的SSM 是骨架SpringBoot 只是幫你省掉一堆 XML 配置的腳手架。這個項目真正值得投入的點在于微信小程序天然適合租房這種「低頻、重決策、需要隨時翻看」的場景用戶不用裝 App掃碼或搜一下就能進(jìn)分享給合租室友也方便。而 SSM 這套組合在國內(nèi)中小型管理系統(tǒng)里沉淀了快十年資料多、坑位明確、招人也好招。所以它不是一個炫技項目是一個能跑通、能交付、能二次開發(fā)的務(wù)實方案。適合誰適合需要一套「房源 CRUD 小程序端展示 預(yù)約流程」最小閉環(huán)的人不適合想直接拿去做高并發(fā) SaaS 的人。2. SSM 后端骨架怎么搭從依賴到第一個房源接口2.1 為什么這個場景下 SSM 仍然夠用先講選型理由不然你搭到一半會懷疑自己。房屋租賃系統(tǒng)的后端壓力其實很小房源列表是讀多寫少預(yù)約記錄是低頻寫入真正的瓶頸往往在圖片存儲和列表分頁上而不是在框架本身。SSM 的 MyBatis 在處理「多條件動態(tài)篩選房源」這種需求時特別順手因為租房篩選條件經(jīng)常變——今天按租金區(qū)間明天加個「是否近地鐵」用 MyBatis 的動態(tài) SQL 改起來比 JPA 的 Criteria 直觀得多。SpringMVC 負(fù)責(zé)把小程序發(fā)來的 JSON 請求接住返回統(tǒng)一格式這套流程非常成熟。常見做法是Maven 多模塊或者單模塊都行我一般單模塊起步包結(jié)構(gòu)按 controller / service / mapper / entity / vo 分。數(shù)據(jù)庫用 MySQL 5.7 或 8.0 都可以字符集統(tǒng)一 utf8mb4因為房源描述里可能有 emoji 或者生僻字。連接池用 Druid監(jiān)控頁面在調(diào)試階段能幫你看清慢 SQL。2.2 最小可運行的依賴與配置下面這份 pom 依賴是這個項目的最小集合多一個都別加加多了啟動慢還容易沖突。!-- pom.xml 關(guān)鍵依賴版本按你本地倉庫已有的穩(wěn)定版來 -- dependencies !-- Spring 核心 MVCSSM 的 S -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.30/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.30/version /dependency !-- MyBatis 與 Spring 整合包別只引 mybatis 本體 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Druid 連接池自帶監(jiān)控 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency !-- JSON 轉(zhuǎn)換小程序端全靠它 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.3/version /dependency /dependencies邏輯說明spring-webmvc 提供 DispatcherServlet 和注解驅(qū)動mybatis-spring 是把 SqlSession 交給 Spring 管理的關(guān)鍵少了它你就得自己寫 SqlSessionFactory 的樣板代碼。參數(shù)上注意 mybatis-spring 2.x 對應(yīng) MyBatis 3.5如果你本地是 1.x 版本和 Spring 5 搭配會報NoClassDefFoundError這是血淚經(jīng)驗別問我怎么知道的。2.3 房源列表接口動態(tài)篩選與分頁房源列表是整個系統(tǒng)被調(diào)用最頻繁的接口小程序首頁、搜索頁、篩選頁都打它。核心是 MyBatis 動態(tài) SQL 加物理分頁。!-- HouseMapper.xml 里的列表查詢用 where 自動處理 and 前綴 -- select idselectHouseList resultTypecom.rent.entity.House SELECT id, title, district, rent, room_type, area, cover_img, status FROM house where if testdistrict ! null and district ! AND district #{district} /if if testminRent ! null AND rent gt; #{minRent} /if if testmaxRent ! null AND rent lt; #{maxRent} /if if testroomType ! null and roomType ! AND room_type #{roomType} /if !-- 只查上架的下架房源不暴露給小程序 -- AND status 1 /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select邏輯說明where標(biāo)簽會自動去掉第一個多余的 AND這是 MyBatis 最實用的特性之一。參數(shù)里 offset 由(pageNum - 1) * pageSize算出pageSize 我一般限制在 10 到 20 之間小程序一屏放不下太多加載更多體驗反而更好。注意rent字段如果是 decimal 類型前端傳參要做一次校驗別讓用戶傳個負(fù)數(shù)進(jìn)來。status 硬編碼在 SQL 里是故意的避免有人忘了加過濾條件把下架房源查出來這種翻車在聯(lián)調(diào)時特別尷尬。3. 微信小程序端怎么接列表加載、登錄與圖片處理3.1 頁面列表加載更多的正確姿勢微信小程序的列表加載更多熱搜里問得很多但很多人寫出來要么重復(fù)請求要么卡在最后一頁死循環(huán)。核心是三個狀態(tài)pageNum、hasMore、loading。// pages/house/list.js Page({ data: { houseList: [], pageNum: 1, pageSize: 10, hasMore: true, loading: false }, onLoad() { this.loadHouseList(); }, // 觸底事件由頁面 onReachBottom 觸發(fā) loadHouseList() { // 正在加載或沒有更多了直接返回防止重復(fù)請求 if (this.data.loading || !this.data.hasMore) return; this.setData({ loading: true }); wx.request({ url: https://your-domain/api/house/list, data: { pageNum: this.data.pageNum, pageSize: this.data.pageSize }, success: (res) { const list res.data.data.list || []; this.setData({ houseList: this.data.houseList.concat(list), pageNum: this.data.pageNum 1, // 返回條數(shù)小于 pageSize說明到底了 hasMore: list.length this.data.pageSize, loading: false }); }, fail: () { // 失敗也要復(fù)位 loading否則永遠(yuǎn)卡住 this.setData({ loading: false }); wx.showToast({ title: 加載失敗, icon: none }); } }); }, onReachBottom() { this.loadHouseList(); } });邏輯說明hasMore的判斷依據(jù)是「本次返回條數(shù)是否等于 pageSize」這是最穩(wěn)的寫法比讓后端返回 total 再算更省一次查詢。參數(shù)上 pageSize 別設(shè)太大小程序 setData 有性能開銷一次 concat 太多數(shù)據(jù)會掉幀。注意 fail 回調(diào)里必須復(fù)位 loading我見過有人漏了這行結(jié)果網(wǎng)絡(luò)抖一下列表就再也加載不出來了用戶只能殺掉小程序重進(jìn)。3.2 微信登錄與后端會話打通小程序端的登錄不能直接用賬號密碼標(biāo)準(zhǔn)流程是wx.login()拿 code后端換 openid再簽發(fā)自己的 token。// 小程序端登錄 wx.login({ success: (res) { if (res.code) { wx.request({ url: https://your-domain/api/user/login, method: POST, data: { code: res.code }, success: (r) { // 后端返回自定義 token存本地 wx.setStorageSync(token, r.data.data.token); } }); } } });后端拿到 code 后用 appid secret code 去微信接口換 openid然后查用戶表沒有就注冊一條最后生成一個 UUID 或 JWT 作為 token 返回。參數(shù)上注意 code 只能用一次五分鐘過期所以別在前端緩存 code。token 存 storage 后后續(xù)每個請求在 header 里帶上后端用攔截器校驗。這里有個坑小程序請求的 header 名建議用Authorization別用中文或特殊字符某些安卓機型會丟 header。3.3 房源圖片的存儲與展示房源圖片是這個項目里最容易拖慢速度的部分。常見做法是圖片存 OSS 或本地靜態(tài)目錄數(shù)據(jù)庫只存 URL。小程序端展示時用image組件的modeaspectFill列表頁一定要用縮略圖別直接加載原圖。!-- 列表頁圖片加 lazy-load 懶加載 -- image src{{item.coverImg}} modeaspectFill lazy-load /參數(shù)說明lazy-load在列表滾動時只加載可視區(qū)域圖片能明顯降低流量和內(nèi)存。如果后端沒做縮略圖至少在上傳時壓縮到 200KB 以內(nèi)寬度 750px 足夠。我一般會在上傳接口里用 Thumbnails 庫壓一道別指望前端壓小程序端壓縮質(zhì)量參差不齊。4. 房源篩選與預(yù)約流程把業(yè)務(wù)閉環(huán)補完整4.1 多條件篩選的參數(shù)設(shè)計篩選是租房系統(tǒng)的靈魂。小程序端一般用頂部下拉或抽屜式篩選面板參數(shù)傳到后端就是 district、minRent、maxRent、roomType 這幾個。設(shè)計上有兩個選擇一是每個條件變化就重新請求二是點「確定」再請求。我推薦后者因為租房用戶經(jīng)常一次調(diào)好幾個條件實時請求會打出一堆廢請求。// 篩選面板確認(rèn)后觸發(fā) onFilterConfirm(e) { const { district, minRent, maxRent, roomType } e.detail; // 重置分頁否則會接著上次的頁碼查 this.setData({ pageNum: 1, hasMore: true, houseList: [], filter: { district, minRent, maxRent, roomType } }); this.loadHouseList(); }邏輯說明篩選條件變化時必須重置 pageNum 和 houseList否則會出現(xiàn)「篩選后列表里混著上次的數(shù)據(jù)」這種玄學(xué)問題。參數(shù)上 minRent 和 maxRent 要做前后端雙重校驗前端限制輸入框只能輸數(shù)字后端再判一次 min max不然 SQL 查出來是空還找不到原因。4.2 預(yù)約看房的表結(jié)構(gòu)與狀態(tài)機預(yù)約流程看著簡單但狀態(tài)一多就容易亂。我一般用一張 appointment 表字段包括 id、house_id、user_id、appoint_time、remark、status、create_time。status 用數(shù)字0 待確認(rèn)、1 已確認(rèn)、2 已完成、3 已取消。CREATE TABLE appointment ( id BIGINT NOT NULL AUTO_INCREMENT, house_id BIGINT NOT NULL COMMENT 房源ID, user_id BIGINT NOT NULL COMMENT 預(yù)約用戶, appoint_time DATETIME NOT NULL COMMENT 預(yù)約看房時間, remark VARCHAR(255) DEFAULT NULL COMMENT 用戶備注, status TINYINT DEFAULT 0 COMMENT 0待確認(rèn) 1已確認(rèn) 2已完成 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_house (house_id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;參數(shù)說明appoint_time 用 DATETIME 而不是時間戳方便后臺直接看。索引加在 house_id 和 user_id 上因為「查某房源的所有預(yù)約」和「查某用戶的所有預(yù)約」是兩個高頻查詢。狀態(tài)流轉(zhuǎn)要在 service 層控制比如已取消的不能改成已完成這種校驗別只靠前端按鈕置灰后端必須再攔一道。4.3 后臺管理端的房源上下架后臺管理端通常是另一個 Web 頁面用同一套 SSM 后端。房源上下架就是改 status 字段但要注意下架時如果有未完成的預(yù)約要么提示管理員要么自動把預(yù)約置為取消并通知用戶。我一般選前者因為自動取消容易引發(fā)糾紛。// HouseService 里的上下架方法 public void updateStatus(Long houseId, Integer status) { House house houseMapper.selectById(houseId); if (house null) { throw new RuntimeException(房源不存在); } // 下架前檢查是否有進(jìn)行中的預(yù)約 if (status 0) { int count appointmentMapper.countActiveByHouse(houseId); if (count 0) { throw new RuntimeException(該房源還有 count 條進(jìn)行中的預(yù)約請先處理); } } houseMapper.updateStatus(houseId, status); }邏輯說明這段代碼的價值在于把業(yè)務(wù)規(guī)則收在 service 層controller 只負(fù)責(zé)接參和返回。參數(shù)上 status 用 0/1 表示下架/上架別用 true/false數(shù)據(jù)庫里 tinyint 更省空間。異常直接拋 RuntimeException 在小型項目里夠用但如果你要統(tǒng)一異常處理配一個ControllerAdvice會更規(guī)范。5. 避坑與排查那些聯(lián)調(diào)時讓人抓狂的問題5.1 小程序請求后端報「不在以下 request 合法域名列表中」現(xiàn)象開發(fā)者工具里勾了「不校驗合法域名」能跑真機預(yù)覽就報這個錯。原因微信小程序正式環(huán)境要求所有請求域名在后臺配置且必須是 HTTPS。解決開發(fā)階段在開發(fā)者工具「詳情-本地設(shè)置」勾選不校驗真機調(diào)試時用「預(yù)覽」并打開調(diào)試模式上線前必須把域名備案、配 HTTPS 證書、在小程序后臺「開發(fā)-開發(fā)設(shè)置-服務(wù)器域名」里加上。注意域名不能帶端口必須是 443。5.2 MyBatis 查詢返回字段為 null但數(shù)據(jù)庫里明明有值現(xiàn)象SQL 在客戶端跑有結(jié)果Java 里查出來對象字段全是 null。原因九成是實體類字段名和數(shù)據(jù)庫列名沒對上比如數(shù)據(jù)庫是cover_img實體類寫的是coverImg但沒開駝峰映射。解決在 MyBatis 配置里加mapUnderscoreToCamelCasetrue或者在 SQL 里用別名。我一般直接開駝峰映射一勞永逸。5.3 小程序 setData 后列表不刷新現(xiàn)象數(shù)據(jù)明明 concat 進(jìn)去了頁面還是舊的。原因直接對 data 里的數(shù)組 push沒有走 setData或者 setData 的 key 寫錯了層級。解決永遠(yuǎn)用this.setData({ houseList: newList })整體賦值別用this.data.houseList.push()。另外注意 setData 是異步的如果你在 setData 回調(diào)外立刻讀 data拿到的還是舊值。5.4 預(yù)約時間存進(jìn)去差 8 小時現(xiàn)象用戶選的是下午 3 點數(shù)據(jù)庫里存的是早上 7 點。原因小程序端new Date()拿到的是本地時間傳到后端 JSON 序列化時如果沒配時區(qū)或者 JDBC 連接串沒加serverTimezone就會偏。解決JDBC URL 里加serverTimezoneAsia/Shanghai后端統(tǒng)一用java.util.Date或LocalDateTime別混用。前端傳時間建議直接傳格式化字符串yyyy-MM-dd HH:mm:ss別傳時間戳省得來回算。5.5 圖片上傳后小程序顯示裂圖現(xiàn)象后臺上傳成功數(shù)據(jù)庫也有 URL小程序里就是裂的。原因URL 是相對路徑或者后端靜態(tài)資源沒映射或者 HTTPS 頁面加載了 HTTP 圖片被攔截。解決數(shù)據(jù)庫存完整 URL后端配WebMvcConfigurer映射靜態(tài)目錄上線后圖片也必須走 HTTPS。另外檢查小程序image組件的 src 有沒有多余空格這種低級錯誤聯(lián)調(diào)時特別常見。6. 進(jìn)階技巧把房源搜索從「能用」做到「好用」到這一步系統(tǒng)基本能跑了但如果你想讓它在答辯或交付時更拿得出手可以加一個關(guān)鍵詞搜索的優(yōu)化。最土的做法是LIKE %關(guān)鍵詞%數(shù)據(jù)量一上來就全表掃描。我的習(xí)慣是給 title 和 district 建全文索引或者至少建普通索引配合前綴匹配。-- 給房源標(biāo)題加全文索引MySQL 5.7 支持中文需要 ngram 解析器 ALTER TABLE house ADD FULLTEXT INDEX ft_title (title) WITH PARSER ngram; -- 查詢時用 MATCH AGAINST比 LIKE 快一個量級 SELECT id, title, rent FROM house WHERE MATCH(title) AGAINST(#{keyword} IN BOOLEAN MODE) AND status 1 LIMIT 20;參數(shù)說明ngram 解析器默認(rèn) token size 是 2也就是按兩個字切詞適合中文。BOOLEAN MODE 支持必須包含、-排除比如用戶搜「朝陽 -合租」就能排除合租。注意全文索引對短詞和停用詞有過濾如果搜不到先看innodb_ft_min_token_size配置。另一個技巧是給列表接口加一層本地緩存。房源列表變化不頻繁用 Guava Cache 或 Caffeine 緩存 30 秒能擋掉大量重復(fù)請求。但要注意上下架和新增房源時主動失效緩存否則用戶會看到已經(jīng)下架的房源這種后悔藥可沒地方買。// 用 Caffeine 做 30 秒本地緩存 private final CacheString, ListHouse listCache Caffeine.newBuilder() .expireAfterWrite(30, TimeUnit.SECONDS) .maximumSize(200) .build(); public ListHouse getList(String key, SupplierListHouse loader) { return listCache.get(key, k - loader.get()); } // 房源變更時手動失效 public void evictListCache() { listCache.invalidateAll(); }邏輯說明expireAfterWrite保證數(shù)據(jù)最多舊 30 秒maximumSize防止內(nèi)存無限漲。參數(shù)上 30 秒是個經(jīng)驗值太短沒效果太長用戶感知明顯。失效方法要在增刪改的 service 里調(diào)用別漏了漏了就是玄學(xué) bug——后臺改了價格小程序半天不更新。最后說個驗證方法拿 Postman 或 Apifox 把列表、詳情、預(yù)約三個接口各跑 50 次看平均響應(yīng)時間。如果列表接口超過 200ms先查慢 SQL 日志八成是沒走索引。我自己的習(xí)慣是每加一個查詢條件就 EXPLAIN 一次看 type 是不是 ALL是 ALL 就回去加索引。這套系統(tǒng)不大但把索引和緩存這兩件事做扎實體驗?zāi)芩﹂_一大半同類項目。希望幫到你。本文還有配套的精品資源點擊獲取