后端:Java Spring Boot與數(shù)據(jù)庫(kù)設(shè)計(jì)實(shí)戰(zhàn)解析)
簡(jiǎn)介這份資源是基于微信小程序的旅游服務(wù)軟件完整項(xiàng)目后端采用Java與SSM框架開(kāi)發(fā)配套數(shù)據(jù)庫(kù)腳本面向計(jì)算機(jī)相關(guān)專(zhuān)業(yè)畢業(yè)設(shè)計(jì)或初學(xué)小程序全棧開(kāi)發(fā)的讀者。項(xiàng)目可在IntelliJ IDEA及微信開(kāi)發(fā)者工具中直接導(dǎo)入運(yùn)行數(shù)據(jù)庫(kù)為MySQL 8.0環(huán)境配置已做精簡(jiǎn)適合快速啟動(dòng)學(xué)習(xí)。壓縮包共22個(gè)文件包括19張界面截圖、2個(gè)源碼壓縮包和1個(gè)SQL數(shù)據(jù)庫(kù)文件整體約61.15MB結(jié)構(gòu)清晰便于按模塊查閱。功能覆蓋旅游攻略、旅游資訊、景點(diǎn)搜索、酒店信息、論壇中心、門(mén)票與酒店預(yù)訂、推薦路線(xiàn)、發(fā)帖互動(dòng)及用戶(hù)管理等從用戶(hù)端到后端管理均有體現(xiàn)可幫助理解小程序與Java后端的數(shù)據(jù)交互、SSM框架分層設(shè)計(jì)和數(shù)據(jù)庫(kù)建模。目前已有2604人學(xué)習(xí)下載對(duì)有畢業(yè)設(shè)計(jì)需求或想系統(tǒng)掌握旅游類(lèi)小程序開(kāi)發(fā)流程的讀者具有較高參考價(jià)值。1. 微信小程序旅游服務(wù)軟件為什么后端Java開(kāi)發(fā)才是項(xiàng)目源碼的重頭戲做旅游類(lèi)微信小程序的人十個(gè)里有八個(gè)卡在同一步小程序端代碼拿到了數(shù)據(jù)庫(kù)文件導(dǎo)入了后端卻怎么都跑不起來(lái)。這個(gè)標(biāo)題其實(shí)點(diǎn)透了畢設(shè)和課設(shè)的核心——前端只是皮相真正的設(shè)計(jì)工作藏在Java后端和數(shù)據(jù)庫(kù)表結(jié)構(gòu)里。這篇文章從項(xiàng)目源碼的組織形式出發(fā)講清楚后端Spring Boot項(xiàng)目怎么搭、數(shù)據(jù)庫(kù)怎么設(shè)計(jì)、小程序怎么對(duì)接再把最容易翻車(chē)的幾個(gè)坑提前給你打上預(yù)防針。適合正在做這類(lèi)項(xiàng)目的學(xué)生也適合想快速上手前后端分離項(xiàng)目實(shí)戰(zhàn)的初級(jí)Java開(kāi)發(fā)。跟著走一遍你會(huì)知道這套方案能不能落地、值不值得往里投入時(shí)間。2. 搭起Spring Boot后端骨架選型、包結(jié)構(gòu)與第一個(gè)能跑的接口2.1 為什么選Spring Boot MyBatis-Plus而不是SSH或Servlet旅游服務(wù)軟件這類(lèi)業(yè)務(wù)核心是用戶(hù)、景點(diǎn)、線(xiàn)路、訂單四組關(guān)系業(yè)務(wù)邏輯并不算復(fù)雜但接口數(shù)量多、表與表之間的關(guān)聯(lián)也多。Spring Boot的自動(dòng)配置和Starter機(jī)制能省掉大量樣板代碼對(duì)拿到項(xiàng)目源碼后想快速跑起來(lái)的場(chǎng)景特別友好。更重要的一點(diǎn)是Spring Boot 2.x MyBatis-Plus的組合在中小型項(xiàng)目和畢業(yè)設(shè)計(jì)里已經(jīng)是事實(shí)標(biāo)準(zhǔn)網(wǎng)上能搜到的報(bào)錯(cuò)和解決方案都很多遇到問(wèn)題不至于卡死。MyBatis-Plus的價(jià)值在于把單表CRUD的代碼幾乎抹平了。景點(diǎn)表、評(píng)論表這類(lèi)低頻變更的單表操作不需要為每個(gè)實(shí)體寫(xiě)一套insert、select、update繼承一個(gè)BaseMapper就完事。旅游項(xiàng)目里最高頻的列表分頁(yè)查詢(xún)用它的Wrapper構(gòu)造條件兩行代碼就能寫(xiě)出來(lái)。我不建議在這個(gè)階段引入過(guò)度復(fù)雜的架構(gòu)。很多人拿到源碼跑不起來(lái)往往不是因?yàn)楣δ芏喽且驗(yàn)槿薉ubbo、Redis緩存、消息隊(duì)列這些重東西。旅游服務(wù)軟件的核心訴求是能跑、能講、能演示技術(shù)棧收斂到Spring Boot MyBatis-Plus MySQL 微信小程序原生開(kāi)發(fā)已經(jīng)足夠撐起整個(gè)項(xiàng)目。2.2 包結(jié)構(gòu)劃分按業(yè)務(wù)模塊劃而非按技術(shù)層次劃拿到源碼后第一件事是看包結(jié)構(gòu)。我見(jiàn)過(guò)不少翻車(chē)的項(xiàng)目把controller、service、mapper按技術(shù)棧堆三層結(jié)果一個(gè)旅游項(xiàng)目幾十個(gè)接口全塞在一個(gè)Controller里改一個(gè)字段要翻十分鐘。常見(jiàn)的做法是按業(yè)務(wù)模塊分包結(jié)構(gòu)大概是這樣com.example.travel ├── config // 全局配置跨域、攔截器、文檔配置 ├── controller // 對(duì)外接口auth、spot、line、order ├── service // 業(yè)務(wù)邏輯 │ └── impl ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 數(shù)據(jù)庫(kù)實(shí)體 ├── dto // 前端入?yún)?duì)象 ├── vo // 返回給前端的視圖對(duì)象 └── common // 統(tǒng)一返回結(jié)構(gòu)、異常處理controller按auth、spot、line、order四個(gè)模塊拆開(kāi)每個(gè)Controller只負(fù)責(zé)一類(lèi)資源的接口。entity和dto分開(kāi)是很多新手容易忽略的細(xì)節(jié)——如果數(shù)據(jù)庫(kù)字段直接暴露給前端一旦表結(jié)構(gòu)調(diào)整小程序端就得跟著改。用VO把返回字段裁剪一下是前后端分離項(xiàng)目實(shí)戰(zhàn)里最基本的解耦手段答辯時(shí)也能講出設(shè)計(jì)依據(jù)。2.3 最小可運(yùn)行配置pom.xml與application.yml不管項(xiàng)目源碼長(zhǎng)什么樣最終跑起來(lái)都靠這兩個(gè)文件。先看pom.xml的核心依賴(lài)我用Maven坐標(biāo)的方式列出來(lái)dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-ui/artifactId version1.7.0/version /dependencyspring-boot-starter-web提供MVC能力mybatis-plus-boot-starter負(fù)責(zé)數(shù)據(jù)庫(kù)操作mysql-connector-java在運(yùn)行期加載驅(qū)動(dòng)。springdoc是Swagger的OpenAPI實(shí)現(xiàn)畢設(shè)答辯時(shí)直接打開(kāi)/swagger-ui.html就能看到一個(gè)可交互的接口文檔頁(yè)面比現(xiàn)場(chǎng)翻代碼講更直觀。注意MyBatis-Plus 3.5.x配Spring Boot 2.7是經(jīng)過(guò)大量項(xiàng)目驗(yàn)證的搭配版本不要隨便升到4.x否則很多配置項(xiàng)會(huì)變。application.yml里最關(guān)鍵的三個(gè)配置是數(shù)據(jù)源、MyBatis-Plus的駝峰映射、日期格式server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0url里必須帶serverTimezoneAsia/Shanghai否則MySQL 8.x會(huì)直接報(bào)時(shí)區(qū)錯(cuò)誤這是最常見(jiàn)的啟動(dòng)失敗原因之一。map-underscore-to-camel-case打開(kāi)后數(shù)據(jù)庫(kù)的spot_name字段能自動(dòng)映射到實(shí)體的spotName屬性不用寫(xiě)繁瑣的resultMap。日志輸出打開(kāi)開(kāi)發(fā)階段每個(gè)SQL都會(huì)打印到控制臺(tái)排查問(wèn)題時(shí)這是第一手線(xiàn)索。到這里一個(gè)最小后端已經(jīng)具備跑起來(lái)的條件。寫(xiě)一個(gè)測(cè)試接口驗(yàn)證RestController RequestMapping(/api/health) public class HealthController { GetMapping public ResultString health() { return Result.success(travel backend is running); } }調(diào)用GET /api/health能返回success就說(shuō)明骨架通了。這里的Result是統(tǒng)一返回包裝類(lèi)后面的所有接口都會(huì)復(fù)用這個(gè)結(jié)構(gòu)。我不建議在這個(gè)階段急著往下寫(xiě)業(yè)務(wù)先把跨域配置和全局異常處理加上。小程序端本身沒(méi)有CORS機(jī)制的限制但如果你用H5方式調(diào)試頁(yè)面CORS就繞不開(kāi)。在config包下加一個(gè)WebMvcConfigurer實(shí)現(xiàn)類(lèi)addCorsMappings里放行所有路徑和本地開(kāi)發(fā)端口即可。提示如果啟動(dòng)時(shí)提示8080端口被占用先執(zhí)行netstat -ano | findstr 8080找到占用進(jìn)程再?zèng)Q定是換端口還是清理進(jìn)程。至此項(xiàng)目源碼的后端部分已經(jīng)從一堆文件變成了一個(gè)能響應(yīng)的服務(wù)。接下來(lái)進(jìn)入真正有設(shè)計(jì)含量的部分——數(shù)據(jù)庫(kù)表結(jié)構(gòu)這決定了景點(diǎn)、線(xiàn)路、訂單三塊業(yè)務(wù)能不能在答辯時(shí)講出邏輯閉環(huán)。3. 旅游業(yè)務(wù)數(shù)據(jù)模型用戶(hù)、景點(diǎn)、訂單三張核心表的設(shè)計(jì)細(xì)節(jié)3.1 用戶(hù)表與微信登錄字段的設(shè)計(jì)不含支付功能的旅游小程序用戶(hù)表不需要存密碼。核心字段是openid、昵稱(chēng)、頭像、手機(jī)號(hào)。openid是微信體系下用戶(hù)的唯一標(biāo)識(shí)小程序端通過(guò)wx.login拿到code后端用code換openid這張表就是整個(gè)登錄體系的錨點(diǎn)。CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信openid, nickname varchar(64) DEFAULT NULL COMMENT 昵稱(chēng), avatar varchar(255) DEFAULT NULL COMMENT 頭像URL, phone varchar(20) DEFAULT NULL COMMENT 手機(jī)號(hào), gender tinyint(1) DEFAULT 0 COMMENT 性別 0未知 1男 2女, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint(1) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用戶(hù)表;openid字段一定要加唯一索引。同一用戶(hù)重復(fù)調(diào)用登錄接口時(shí)后端應(yīng)該按openid去更新用戶(hù)信息而不是重復(fù)插入。deleted字段配合MyBatis-Plus的邏輯刪除用戶(hù)注銷(xiāo)時(shí)不會(huì)物理刪除數(shù)據(jù)歷史訂單還能關(guān)聯(lián)上。create_time和update_time直接由數(shù)據(jù)庫(kù)維護(hù)不需要在實(shí)體里手動(dòng)賦值MyBatis-Plus的insert和update語(yǔ)句會(huì)自動(dòng)跳過(guò)這兩個(gè)字段。3.2 景點(diǎn)與線(xiàn)路別把兩張表合成一張旅游項(xiàng)目的核心資源是景點(diǎn)和線(xiàn)路。景點(diǎn)是靜態(tài)資源線(xiàn)路是動(dòng)態(tài)組合。新手常見(jiàn)的設(shè)計(jì)錯(cuò)誤是把線(xiàn)路直接做成一個(gè)字段存景點(diǎn)ID列表比如line_detail字段存1,2,3,4看起來(lái)方便但要查某個(gè)景點(diǎn)被哪些線(xiàn)路包含時(shí)必須用LIKE %2%去模糊匹配——不僅慢還會(huì)匹配錯(cuò)誤。正確的做法是線(xiàn)路主表加一張線(xiàn)路-景點(diǎn)關(guān)聯(lián)表CREATE TABLE line_spot ( id bigint(20) NOT NULL AUTO_INCREMENT, line_id bigint(20) NOT NULL COMMENT 線(xiàn)路ID, spot_id bigint(20) NOT NULL COMMENT 景點(diǎn)ID, sort_order int(11) DEFAULT 0 COMMENT 游覽順序, PRIMARY KEY (id), KEY idx_line_id (line_id), KEY idx_spot_id (spot_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT線(xiàn)路景點(diǎn)關(guān)聯(lián)表;sort_order字段記錄景點(diǎn)的游覽順序這是旅游線(xiàn)路區(qū)別于一般商品列表的地方。查詢(xún)線(xiàn)路詳情時(shí)按line_id過(guò)濾、sort_order排序就能還原出一條完整的游覽動(dòng)線(xiàn)。關(guān)聯(lián)表是數(shù)據(jù)庫(kù)多對(duì)多關(guān)系的標(biāo)準(zhǔn)拆法答辯時(shí)能講清楚這一點(diǎn)比堆技術(shù)名詞更讓老師信服。景點(diǎn)表本身相對(duì)簡(jiǎn)單但要注意幾個(gè)字段的類(lèi)型取舍。景點(diǎn)描述用TEXT不要用VARCHAR(255)因?yàn)檎鎸?shí)景點(diǎn)的介紹文案動(dòng)輒幾百字。封面圖URL用VARCHAR(512)現(xiàn)在云存儲(chǔ)的URL普遍帶簽名參數(shù)長(zhǎng)度經(jīng)常超255。景點(diǎn)經(jīng)緯度用DECIMAL(10,6)而不是FLOAT避免浮點(diǎn)精度導(dǎo)致地圖定位偏移。3.3 訂單表價(jià)格快照與狀態(tài)機(jī)的設(shè)計(jì)價(jià)值訂單表是整個(gè)項(xiàng)目里數(shù)據(jù)設(shè)計(jì)含金量最高的部分。旅游訂單的特點(diǎn)是下單時(shí)的價(jià)格和出行時(shí)的價(jià)格可能不同因此必須做價(jià)格快照。如果訂單表只存line_id再去關(guān)聯(lián)查詢(xún)當(dāng)前價(jià)格一旦后臺(tái)改了線(xiàn)路價(jià)格歷史訂單就失真了。CREATE TABLE order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 訂單號(hào), user_id bigint(20) NOT NULL COMMENT 用戶(hù)ID, line_id bigint(20) DEFAULT NULL COMMENT 線(xiàn)路ID, spot_id bigint(20) DEFAULT NULL COMMENT 景點(diǎn)ID單景點(diǎn)門(mén)票時(shí)使用, title varchar(100) NOT NULL COMMENT 訂單展示名稱(chēng)快照, cover varchar(512) DEFAULT NULL COMMENT 封面圖快照, price decimal(10,2) NOT NULL COMMENT 成交單價(jià)快照, quantity int(11) DEFAULT 1 COMMENT 數(shù)量, total_amount decimal(10,2) NOT NULL COMMENT 成交總價(jià), contact_name varchar(20) NOT NULL COMMENT 聯(lián)系人, contact_phone varchar(20) NOT NULL COMMENT 聯(lián)系電話(huà), travel_date date DEFAULT NULL COMMENT 出行日期, status tinyint(2) DEFAULT 0 COMMENT 0待支付 1已支付 2已完成 3已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint(1) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT旅游訂單表;title、cover、price這三個(gè)快照字段是重點(diǎn)。下單那一刻的線(xiàn)路名稱(chēng)、封面、價(jià)格被復(fù)制到訂單表里之后后臺(tái)修改線(xiàn)路信息訂單數(shù)據(jù)不受影響。這種做法在電商系統(tǒng)里叫快照模式是答辯時(shí)實(shí)打?qū)嵉脑O(shè)計(jì)亮點(diǎn)。status字段用tinyint存數(shù)字狀態(tài)值比用字符串更節(jié)省空間也便于后期擴(kuò)展?fàn)顟B(tài)機(jī)。建議在Java代碼里用枚舉類(lèi)維護(hù)這些狀態(tài)值而不是在業(yè)務(wù)代碼里寫(xiě)0、1、2這種魔法數(shù)字。3.4 數(shù)據(jù)庫(kù)文件交付SQL腳本的四個(gè)細(xì)節(jié)項(xiàng)目源碼里帶的數(shù)據(jù)庫(kù)文件通常是.sql格式。交付時(shí)要注意四個(gè)細(xì)節(jié)。第一導(dǎo)出要包含CREATE DATABASE語(yǔ)句很多初學(xué)者拿到庫(kù)文件不知道要先建庫(kù)。第二初始化數(shù)據(jù)要覆蓋三個(gè)層級(jí)一個(gè)測(cè)試用戶(hù)、至少10條景點(diǎn)記錄、5條線(xiàn)路記錄這樣小程序端一打開(kāi)就有內(nèi)容可看。第三字符集統(tǒng)一用utf8mb4否則用戶(hù)昵稱(chēng)里的emoji表情會(huì)插入失敗。第四SQL文件里不要包含本地絕對(duì)路徑或數(shù)據(jù)庫(kù)密碼明文保護(hù)基本信息。導(dǎo)入數(shù)據(jù)庫(kù)時(shí)最常見(jiàn)的報(bào)錯(cuò)是排序規(guī)則不兼容。utf8mb4_0900_ai_ci是MySQL 8.0的默認(rèn)排序規(guī)則如果導(dǎo)出用的8.0而本地跑的是5.7會(huì)直接導(dǎo)入失敗。解決辦法是導(dǎo)出時(shí)在Navicat或命令行里指定排序規(guī)則為utf8mb4_general_ci或者導(dǎo)入前用文本編輯器批量替換文件里的規(guī)則串。注意數(shù)據(jù)庫(kù)文件和你自己的后端代碼一樣是項(xiàng)目源碼的重要組成部分。答辯前導(dǎo)出一份干凈的初始數(shù)據(jù)備份比任何講解都更有說(shuō)服力。4. 小程序端與Java后端對(duì)接登錄、列表、詳情頁(yè)的最小閉環(huán)4.1 wx.login換openid會(huì)話(huà)token的處理流程微信小程序的登錄跟傳統(tǒng)網(wǎng)頁(yè)完全不同沒(méi)有cookie機(jī)制也不適合每次請(qǐng)求都拿著openid去查用戶(hù)表。標(biāo)準(zhǔn)做法是小程序wx.login拿到code請(qǐng)求后端/auth/login接口后端拿著code向微信服務(wù)器換openid再用openid查詢(xún)或創(chuàng)建用戶(hù)最后生成一個(gè)自定義token返回給小程序端。小程序端把token存進(jìn)storage后續(xù)所有請(qǐng)求都在header里帶上token。后端登錄接口的Controller層代碼大致是這樣PostMapping(/login) public ResultLoginVO login(RequestBody LoginDTO dto) { String code dto.getCode(); String openid wechatService.code2openid(code); User user userService.findOrCreate(openid); String token jwtUtil.generateToken(user.getId()); return Result.success(new LoginVO(token, user)); }code2openid方法封裝了向微信服務(wù)器發(fā)請(qǐng)求的邏輯用Spring的RestTemplate即可不需要額外引入HTTP庫(kù)。微信接口返回的是JSON其中openid和session_key是核心字段。session_key不要返回給小程序端它是后續(xù)解密手機(jī)號(hào)等敏感操作的密鑰暴露出去會(huì)有安全風(fēng)險(xiǎn)。token生成用JWT而不是UUID因?yàn)镴WT自包含后端不需要把token存在內(nèi)存里驗(yàn)證時(shí)用密鑰解析即可適合小程序這種無(wú)狀態(tài)請(qǐng)求模型。JWT載荷里只放用戶(hù)ID和過(guò)期時(shí)間過(guò)期時(shí)間建議2小時(shí)旅游類(lèi)應(yīng)用用戶(hù)使用頻率不高過(guò)期后重登一次成本很低。小程序端的請(qǐng)求封裝也很關(guān)鍵。wx.request每寫(xiě)一次就要重復(fù)設(shè)置header、處理錯(cuò)誤碼所以常見(jiàn)做法是封裝一個(gè)request.js統(tǒng)一管理const request (url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: getApp().globalData.baseUrl url, method: method, data: data, header: { Authorization: Bearer wx.getStorageSync(token) }, success: (res) { if (res.statusCode 401) { wx.navigateTo({ url: /pages/login/login }); reject(res); } else if (res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: reject }); }); };這段代碼的邏輯是所有請(qǐng)求自動(dòng)帶上Authorization頭后端攔截器對(duì)未攜帶token或token過(guò)期的請(qǐng)求返回401小程序端統(tǒng)一捕獲401并跳轉(zhuǎn)登錄頁(yè)。業(yè)務(wù)狀態(tài)用code0表示成功非0時(shí)用toast直接展示后端返回的msg。這樣前端不需要為每個(gè)接口單獨(dú)寫(xiě)錯(cuò)誤處理這個(gè)模式在前后端分離項(xiàng)目里是通用做法能把接口對(duì)接的代碼量減少三分之一。4.2 景點(diǎn)列表分頁(yè)后端參數(shù)與小程序端渲染的配合旅游小程序首頁(yè)通常是景點(diǎn)列表涉及分頁(yè)、搜索、排序三個(gè)參數(shù)。后端接口設(shè)計(jì)時(shí)分頁(yè)參數(shù)我習(xí)慣用一個(gè)統(tǒng)一的PageDTO作為入?yún)ublic class PageDTO { private Integer pageNum 1; private Integer pageSize 10; private String keyword; private String sort; }pageNum從1開(kāi)始pageSize上限設(shè)置20防止有人一次性拉全量數(shù)據(jù)把數(shù)據(jù)庫(kù)拖垮。keyword用于按名稱(chēng)模糊搜索景點(diǎn)sort支持default和price兩種排序模式。后端用MyBatis-Plus的Page對(duì)象做分頁(yè)查詢(xún)public PageSpotVO getSpotPage(PageDTO dto) { LambdaQueryWrapperSpot wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(dto.getKeyword())) { wrapper.like(Spot::getName, dto.getKeyword()); } PageSpot page spotMapper.selectPage( new Page(dto.getPageNum(), dto.getPageSize()), wrapper); PageSpotVO result new Page(dto.getPageNum(), dto.getPageSize()); result.setTotal(page.getTotal()); result.setRecords(spotConverter.toVOList(page.getRecords())); return result; }LambdaQueryWrapper是MyBatis-Plus的條件構(gòu)造器spotMapper.selectPage第一個(gè)參數(shù)是分頁(yè)對(duì)象第二個(gè)是查詢(xún)條件。這里把實(shí)體轉(zhuǎn)成VO再返回避免數(shù)據(jù)庫(kù)字段直接暴露給前端。total總數(shù)對(duì)小程序端的滾動(dòng)加載特別重要——當(dāng)已加載條數(shù)大于等于total時(shí)就該停止上拉分頁(yè)請(qǐng)求。小程序端用onReachBottom實(shí)現(xiàn)觸底加載維護(hù)pageNum和pageSize兩個(gè)data字段每次請(qǐng)求返回后累加列表數(shù)據(jù)而不是覆蓋。這里有一個(gè)新手常犯的錯(cuò)誤pageNum放在data里但請(qǐng)求回調(diào)里忘記在成功后再1導(dǎo)致同一頁(yè)數(shù)據(jù)被重復(fù)加載。4.3 圖片資源與后端靜態(tài)資源映射旅游項(xiàng)目的圖片量很大景點(diǎn)封面、線(xiàn)路詳情圖、用戶(hù)頭像每張圖都走后端轉(zhuǎn)發(fā)不現(xiàn)實(shí)。常見(jiàn)做法是后端只存圖片URL圖片本身放云存儲(chǔ)。開(kāi)發(fā)階段圖片放到后端項(xiàng)目的static目錄下通過(guò)Spring Boot的靜態(tài)資源映射訪(fǎng)問(wèn)。Spring Boot默認(rèn)把classpath:/static/映射為根路徑所以把upload文件夾放在resources/static/upload下訪(fǎng)問(wèn)http://localhost:8080/upload/spot1.jpg就能直接拿到圖片。圖片上傳接口是另一個(gè)容易踩坑的點(diǎn)。小程序端用wx.uploadFile上傳后端接收MultipartFile。Spring Boot的單個(gè)文件大小默認(rèn)限制是1MB景點(diǎn)高清圖動(dòng)輒3MB以上必須提前調(diào)大限制spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MBmax-file-size限制單個(gè)文件max-request-size限制整個(gè)請(qǐng)求體。文件上傳成功后返回給前端的應(yīng)該是可訪(fǎng)問(wèn)的完整URL而不是一個(gè)相對(duì)路徑。我踩過(guò)這個(gè)坑接口返回了upload/20240601.jpg小程序端直接往域名后面拼接卻404原因就是沒(méi)有正確拼接請(qǐng)求根路徑。注意上線(xiàn)前圖片資源必須遷移到對(duì)象存儲(chǔ)并開(kāi)啟CDN加速本地static目錄方案只適合開(kāi)發(fā)和演示環(huán)境。5. 微信小程序旅游后端避坑指南5個(gè)讓我翻車(chē)的真實(shí)案例5.1 真機(jī)預(yù)覽請(qǐng)求全部失敗但開(kāi)發(fā)工具里一切正常現(xiàn)象小程序在開(kāi)發(fā)者工具里調(diào)后端接口全部成功一換成真機(jī)預(yù)覽所有請(qǐng)求全部超時(shí)報(bào)錯(cuò)。原因微信小程序的網(wǎng)絡(luò)請(qǐng)求有嚴(yán)格的域名限制。開(kāi)發(fā)者工具里可以勾選不校驗(yàn)合法域名來(lái)跳過(guò)限制但真機(jī)上這個(gè)開(kāi)關(guān)不起作用。后端localhost地址在真機(jī)上根本找不到而且小程序要求所有請(qǐng)求域名必須是HTTPS且在后臺(tái)配置過(guò)白名單。解決開(kāi)發(fā)階段用手機(jī)連電腦的局域網(wǎng)IP訪(fǎng)問(wèn)后端把后端啟動(dòng)host改成0.0.0.0前端baseUrl改成http://局域網(wǎng)IP:8080。要真機(jī)調(diào)試HTTPS效果就用映射工具把本地的8080端口映射成一個(gè)公網(wǎng)HTTPS地址再把域名加到小程序后臺(tái)的request合法域名里。最終上線(xiàn)前必須配好備案域名和SSL證書(shū)云廠(chǎng)商一般都有免費(fèi)證書(shū)申請(qǐng)入口在Nginx里配置HTTPS是固定套路這部分不是玄學(xué)照著官方文檔配一遍就能跑通。5.2 訂單時(shí)間總是差了8小時(shí)現(xiàn)象后端返回的create_time字段小程序端顯示比本地時(shí)間慢了8小時(shí)。數(shù)據(jù)庫(kù)里存的時(shí)間是正確的響應(yīng)JSON里卻變成了帶T的UTC格式。原因Jackson在序列化LocalDateTime時(shí)默認(rèn)使用UTC時(shí)區(qū)。后端服務(wù)器的系統(tǒng)時(shí)區(qū)、MySQL連接串里的serverTimezone、Jackson的時(shí)區(qū)配置三者不一致就會(huì)造成時(shí)間偏移。解決保持三重配置一致。第一application.yml里加spring.jackson.time-zoneGMT8和date-formatyyyy-MM-dd HH:mm:ss。第二MySQL連接串保持serverTimezoneAsia/Shanghai。第三實(shí)體里的時(shí)間字段加JsonFormat(patternyyyy-MM-dd HH:mm:ss, timezoneGMT8)注解兜底。改完這三處重啟后端再測(cè)一遍時(shí)間就對(duì)齊了。5.3 數(shù)據(jù)庫(kù)連接池耗盡頁(yè)面隨機(jī)卡死現(xiàn)象小程序端高頻請(qǐng)求后后端日志出現(xiàn)Connection is not available, request timed out部分接口偶發(fā)性超時(shí)重啟后才恢復(fù)。原因這是典型的數(shù)據(jù)庫(kù)連接池被打滿(mǎn)。某個(gè)Service方法內(nèi)加了事務(wù)注解事務(wù)內(nèi)又調(diào)用了外部HTTP接口HTTP接口響應(yīng)慢數(shù)據(jù)庫(kù)連接被事務(wù)長(zhǎng)時(shí)間占用。默認(rèn)連接池大小只有1010個(gè)連接都被慢請(qǐng)求占滿(mǎn)后新請(qǐng)求全部排隊(duì)等連接。解決第一排查所有Transactional方法事務(wù)內(nèi)不要做遠(yuǎn)程HTTP調(diào)用先完成數(shù)據(jù)庫(kù)操作再調(diào)外部接口或者用編程式事務(wù)精確控制邊界。第二把連接池調(diào)大并加監(jiān)控Spring Boot默認(rèn)使用HikariCPspring: datasource: hikari: maximum-pool-size: 30 minimum-idle: 10 connection-timeout: 30000把maximum-pool-size調(diào)到30只是緩解癥狀根因還是事務(wù)內(nèi)不要做遠(yuǎn)程調(diào)用。可以用一個(gè)AOP切面把執(zhí)行超過(guò)1秒的事務(wù)方法打印到日志里快速定位慢事務(wù)才能真正解決問(wèn)題。5.4 用戶(hù)連續(xù)點(diǎn)擊下單生成了兩條重復(fù)訂單現(xiàn)象用戶(hù)在小程序端雙擊立即預(yù)訂按鈕后端收到兩個(gè)幾乎同時(shí)到達(dá)的下單請(qǐng)求數(shù)據(jù)庫(kù)里出現(xiàn)了兩條相同的訂單記錄。原因前端只做了按鈕loading狀態(tài)但loading生效前的瞬間兩個(gè)請(qǐng)求已經(jīng)發(fā)出。后端沒(méi)有做任何冪等校驗(yàn)insert語(yǔ)句被重復(fù)執(zhí)行。解決訂單表上已經(jīng)建了order_no唯一索引但如果訂單號(hào)生成邏輯放在事務(wù)內(nèi)用時(shí)間戳生成同一毫秒的兩次請(qǐng)求仍可能撞號(hào)。常見(jiàn)的做法是前端在點(diǎn)擊下單時(shí)生成一個(gè)requestId傳給后端后端判斷requestId是否處理過(guò)處理過(guò)就直接返回已有訂單。更穩(wěn)的方案是在訂單表上建(user_id, line_id, travel_date)的聯(lián)合唯一索引讓重復(fù)插入直接拋異常再在Service層捕獲異常返回請(qǐng)勿重復(fù)操作。這個(gè)方案不依賴(lài)前端配合數(shù)據(jù)庫(kù)層直接兜底。5.5 圖片加載白屏控制臺(tái)報(bào)錯(cuò)圖片鏈接未授權(quán)現(xiàn)象小程序端打開(kāi)景點(diǎn)詳情頁(yè)輪播圖區(qū)域白屏控制臺(tái)提示下載圖片失敗。原因圖片URL是后端拼接的域名可能是localhost或IP地址微信小程序?qū)W(wǎng)絡(luò)圖片有緩存和域名校驗(yàn)機(jī)制。更隱蔽的原因是圖片URL中包含了符號(hào)被解析成多個(gè)參數(shù)實(shí)際訪(fǎng)問(wèn)的URL被截?cái)嗔?。解決第一所有圖片URL統(tǒng)一返回完整路徑即https://域名/ 相對(duì)路徑不要返回相對(duì)路徑讓前端自己拼。第二后端生成URL時(shí)對(duì)查詢(xún)參數(shù)做encodeURIComponent編碼前端拿到后用decodeURIComponent還原避免特殊字符被截?cái)?。第三如果用了云存?chǔ)圖片域名必須加入小程序后臺(tái)的downloadFile合法域名否則真機(jī)上永遠(yuǎn)加載不出來(lái)。6. 項(xiàng)目驗(yàn)收前的三個(gè)驗(yàn)證技巧讓代碼在答辯時(shí)不出丑到這里項(xiàng)目已經(jīng)能跑起來(lái)了。但能跑和能演示之間還有一段距離。我總結(jié)三個(gè)自己驗(yàn)收前必做的驗(yàn)證動(dòng)作每個(gè)都能提前暴露問(wèn)題。第一個(gè)是接口全量冒煙測(cè)試。用Postman或Apifox把項(xiàng)目里的所有接口按業(yè)務(wù)鏈路串起來(lái)跑一遍從登錄拿到token到創(chuàng)建訂單、查詢(xún)訂單、取消訂單覆蓋整條主流程。重點(diǎn)觀察返回狀態(tài)碼和響應(yīng)耗時(shí)。我習(xí)慣在Postman里設(shè)一個(gè)全局變量token登錄接口里用一段test腳本自動(dòng)提取token后續(xù)請(qǐng)求全部引用{{token}}這樣能真實(shí)模擬前端調(diào)用順序。第二個(gè)是事務(wù)回滾驗(yàn)證。找一個(gè)修改類(lèi)接口故意讓SQL執(zhí)行失敗比如給一個(gè)不存在的ID傳參看接口是否正確返回錯(cuò)誤碼以及數(shù)據(jù)庫(kù)里是否發(fā)生了部分更新。這個(gè)驗(yàn)證直接對(duì)應(yīng)答辯時(shí)老師最常問(wèn)的問(wèn)題如果下單流程里第二步失敗了第一步的訂單會(huì)留下來(lái)嗎在Service層加上Transactional并驗(yàn)證回滾后你就能有底氣地回答。第三個(gè)是日志檢查。把后端控制臺(tái)日志打開(kāi)跑一遍核心流程看有沒(méi)有MyBatis打印出select *全量查詢(xún)有沒(méi)有重復(fù)執(zhí)行的相同SQL。全量查詢(xún)?cè)诰包c(diǎn)列表這種接口里非常常見(jiàn)原因通常是實(shí)體關(guān)聯(lián)字段沒(méi)做懶加載。把慢SQL日志打開(kāi)你會(huì)發(fā)現(xiàn)很多接口的耗時(shí)集中在某幾張表的關(guān)聯(lián)上這時(shí)候在JOIN字段上補(bǔ)索引比加Redis緩存更直接有效。最后分享一個(gè)我的習(xí)慣答辯前一周把數(shù)據(jù)庫(kù)導(dǎo)出一份干凈的備份放到項(xiàng)目源碼的database目錄下。這樣不管演示時(shí)數(shù)據(jù)被改成什么樣隨時(shí)能一鍵還原回初始狀態(tài)。這個(gè)習(xí)慣救過(guò)我很多次也希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取