物救助系統(tǒng):狀態(tài)流轉(zhuǎn)與并發(fā)控制實(shí)戰(zhàn))
簡介面向高校計(jì)算機(jī)專業(yè)畢業(yè)設(shè)計(jì)的流浪動(dòng)物救助數(shù)據(jù)庫項(xiàng)目使用SpringBoot框架與微信小程序技術(shù)構(gòu)建覆蓋后端Java服務(wù)、小程序前端展示、數(shù)據(jù)庫表結(jié)構(gòu)及完整部署說明。系統(tǒng)圍繞流浪動(dòng)物信息登記、領(lǐng)養(yǎng)申請與救助記錄等核心功能展開能幫助開發(fā)者深入理解前后端分離架構(gòu)、小程序生命周期及接口聯(lián)調(diào)流程。壓縮包共681個(gè)文件以97個(gè)Java源碼、96個(gè)Vue頁面、61個(gè)JavaScript腳本、22個(gè)WXSS樣式與21個(gè)WXML模板等小程序開發(fā)文件為主附有演示視頻、一鍵啟動(dòng)腳本、SQL初始化文件和多種圖標(biāo)素材整體體積約49.97MB目錄組織層次分明便于按模塊檢索學(xué)習(xí)。項(xiàng)目已在Windows10/11環(huán)境嚴(yán)格調(diào)試答辯評審獲得97分配套使用文檔、部署教程及全部項(xiàng)目資料下載配置后即可運(yùn)行既適用于畢業(yè)設(shè)計(jì)參考也能作為期末課程作業(yè)的完整示例。目前已有208人學(xué)習(xí)下載對于正在尋找全棧實(shí)戰(zhàn)項(xiàng)目或入門小程序開發(fā)的Java學(xué)習(xí)者這套項(xiàng)目能提供從代碼到部署的完整參考。1. 流浪動(dòng)物救助程序難點(diǎn)不是CRUD而是狀態(tài)流轉(zhuǎn)拿到這個(gè)壓縮包第一眼看到的是五樣?xùn)|西SpringBoot后端、微信小程序端、救助數(shù)據(jù)庫、使用文檔、演示視頻。畢業(yè)設(shè)計(jì)做成這個(gè)標(biāo)題真正要回答的問題不是“有多少張表”而是當(dāng)一只動(dòng)物同時(shí)有三個(gè)人提交領(lǐng)養(yǎng)申請時(shí)系統(tǒng)怎樣保證只有一個(gè)人能領(lǐng)養(yǎng)成功。我第一次做這類項(xiàng)目也會先把動(dòng)物檔案、領(lǐng)養(yǎng)申請、救助記錄各建一張表跑起來之后才發(fā)現(xiàn)狀態(tài)字段不一致會帶來一堆臟數(shù)據(jù)。這篇文章會把數(shù)據(jù)庫怎么設(shè)計(jì)、SpringBoot接口怎么把狀態(tài)流轉(zhuǎn)鎖住、小程序端怎么把列表和表單串起來講清楚適合準(zhǔn)備SpringBoot框架面試題時(shí)想找一個(gè)完整業(yè)務(wù)鏈路的讀者也適合開始接觸微信小程序開發(fā)想要一個(gè)能答辯的項(xiàng)目樣板的人。2. 拆解救助數(shù)據(jù)庫從ER圖到SpringBoot實(shí)體與表結(jié)構(gòu)做數(shù)據(jù)庫課程設(shè)計(jì)的時(shí)候習(xí)慣先從ER圖開始。流浪動(dòng)物救助這個(gè)業(yè)務(wù)域第一版只需要四張表動(dòng)物檔案、領(lǐng)養(yǎng)申請、救助記錄、小程序用戶。實(shí)體之間的關(guān)系不復(fù)雜真正的復(fù)雜度來自“狀態(tài)聯(lián)動(dòng)”領(lǐng)養(yǎng)申請審核通過時(shí)動(dòng)物要從待領(lǐng)養(yǎng)變成已領(lǐng)養(yǎng)同時(shí)該動(dòng)物其他待審核的申請必須全部作廢。這個(gè)動(dòng)作如果只在頁面里改一個(gè)字段數(shù)據(jù)很快就會臟。表名業(yè)務(wù)含義關(guān)鍵字段animal動(dòng)物檔案id, name, species, gender, status, cover_url, locationadopt_application領(lǐng)養(yǎng)申請id, animal_id, user_id, applicant_name, phone, statusrescue_record救助記錄id, animal_id, rescuer_name, phone, address, descriptionsys_user小程序用戶id, openid, nickname, phone, role救助記錄和動(dòng)物檔案是邏輯上的多對一一只動(dòng)物可能被同一個(gè)人救助過多次救助記錄只負(fù)責(zé)留痕不參與領(lǐng)養(yǎng)狀態(tài)判斷。領(lǐng)養(yǎng)申請和動(dòng)物檔案是多對一和用戶也是一對多。這個(gè)項(xiàng)目不建物理外鍵用邏輯外鍵加索引就夠了否則小程序端刪除種子數(shù)據(jù)的時(shí)候會頻繁觸發(fā)外鍵約束演示反而卡住。2.1 救助域的三類核心實(shí)體與關(guān)系實(shí)體關(guān)系理清之后第二件事是給狀態(tài)字段定邊界。動(dòng)物狀態(tài)和申請狀態(tài)不能混在一個(gè)枚舉里否則業(yè)務(wù)邏輯會越來越繞。我的建議是動(dòng)物表單獨(dú)維護(hù) status領(lǐng)養(yǎng)申請表單獨(dú)維護(hù) status兩個(gè)狀態(tài)通過業(yè)務(wù)規(guī)則同步。2.1.1 動(dòng)物狀態(tài)與申請狀態(tài)的邊界動(dòng)物狀態(tài)建議用四個(gè)值0 待審核、1 待領(lǐng)養(yǎng)、2 已領(lǐng)養(yǎng)、3 已下架。領(lǐng)養(yǎng)申請狀態(tài)用四個(gè)值0 待審核、1 已通過、2 已拒絕、3 已取消。救援人員提交救助記錄后默認(rèn)進(jìn)入待審核管理員審核通過后變成待領(lǐng)養(yǎng)用戶提交領(lǐng)養(yǎng)申請后管理員審核通過動(dòng)物狀態(tài)變成已領(lǐng)養(yǎng)同時(shí)把其他待審核申請置為已拒絕。這里最容易錯(cuò)的是認(rèn)為“動(dòng)物狀態(tài) 最新一條申請狀態(tài)”。實(shí)際上申請狀態(tài)只是流程單據(jù)狀態(tài)動(dòng)物狀態(tài)才是業(yè)務(wù)狀態(tài)。比如動(dòng)物體檢不過被下架狀態(tài)變成 3這時(shí)它的待審核申請仍然可能是 0需要服務(wù)層在下架時(shí)統(tǒng)一處理。把狀態(tài)邊界寫在表注釋里比寫在代碼注釋里更有效因?yàn)榻ū砟_本會被多人反復(fù)導(dǎo)入。2.2 用Navicat建表的字段與索引設(shè)計(jì)建表腳本可以直接導(dǎo)入Navicat執(zhí)行。我一般會關(guān)閉外鍵檢查避免初始化順序?qū)е聦?dǎo)入失敗。下面是核心的建表語句已經(jīng)去掉了不必要的冗余字段CREATE TABLE animal ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL DEFAULT COMMENT 動(dòng)物昵稱, species VARCHAR(20) NOT NULL COMMENT cat/dog/rabbit, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1公 2母, age_months INT NOT NULL DEFAULT 0, health_status VARCHAR(100) NOT NULL DEFAULT 待體檢, location VARCHAR(120) NOT NULL DEFAULT COMMENT 發(fā)現(xiàn)地或收容地址, cover_url VARCHAR(255) NOT NULL DEFAULT , status TINYINT NOT NULL DEFAULT 0 COMMENT 0待審核 1待領(lǐng)養(yǎng) 2已領(lǐng)養(yǎng) 3已下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_status_create (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT動(dòng)物檔案; CREATE TABLE adopt_application ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, animal_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, applicant_name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, reason VARCHAR(500) NOT NULL DEFAULT , housing_info VARCHAR(500) NOT NULL DEFAULT COMMENT 居住情況, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待審核 1已通過 2已拒絕 3已取消, apply_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME DEFAULT NULL COMMENT 審核時(shí)間, KEY idx_animal_status (animal_id, status), KEY idx_user_apply (user_id, apply_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT領(lǐng)養(yǎng)申請表;id 使用 BIGINT UNSIGNED避免以后數(shù)據(jù)量超過 INT 上限。狀態(tài)字段用 TINYINT 而不是 INT節(jié)省行空間InnoDB 行越小單頁能存的記錄越多列表查詢越占優(yōu)勢。create_time 和 update_time 交給數(shù)據(jù)庫默認(rèn)值維護(hù)Java 實(shí)體里只做讀取不手動(dòng) set。索引方面首頁列表最常見的查詢是where status ? order by create_time desc所以建聯(lián)合索引 idx_status_create領(lǐng)養(yǎng)審核頁最常見的查詢是where animal_id ? and status ?所以建 idx_animal_status。這里不建議在 animal_id 和 status 上建唯一索引因?yàn)闃I(yè)務(wù)規(guī)則要求的是“同一動(dòng)物只有一個(gè)狀態(tài)為 0 的待審核申請”MySQL 普通唯一索引做不到只對某個(gè)狀態(tài)值唯一。要不依賴數(shù)據(jù)庫約束把這個(gè)校驗(yàn)放到 Service 層事務(wù)里邏輯更直觀。2.3 MyBatis-Plus還是Spring Data JPA選型與代碼落地做 SpringBoot 項(xiàng)目ORM 選 MyBatis-Plus 最常見。理由很實(shí)際單表 CRUD 完全不用寫 SQL繼承 BaseMapper 就有 selectById、selectPage、updateById聯(lián)表查詢寫 Xml 也能接住。Spring Data JPA 的派生查詢寫起來也省事但面試時(shí)導(dǎo)師更容易追問代理對象、懶加載、N1 問題畢業(yè)設(shè)計(jì)時(shí)間緊的話沒必要給自己加戲。pom 里引入 mybatis-plus-boot-starter 就行版本選你之前跑通過的一個(gè) 3.5.x不要追最新SpringBoot 版本太高反而會出現(xiàn)分頁插件攔截器不兼容的情況。實(shí)體類和表字段做映射注意 MyBatis-Plus 默認(rèn)把駝峰轉(zhuǎn)下劃線所以 Java 屬性寫 createTime表字段寫 create_time不需要額外配置。TableName(animal) public class Animal { TableId(type IdType.AUTO) private Long id; private String name; private String species; private Integer gender; private Integer ageMonths; private String healthStatus; private String location; private String coverUrl; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }接口層再寫一個(gè) Mapper 接口繼承 BaseMapper。TableName指定表名TableId(type IdType.AUTO)對應(yīng)自增主鍵。animal 表里有 localDateTime 字段數(shù)據(jù)庫 URL 參數(shù)里記得配 serverTimezoneAsia/Shanghai否則本機(jī)時(shí)間和服務(wù)器時(shí)間對不上演示視頻里會出現(xiàn)“剛提交的申請時(shí)間比當(dāng)前時(shí)間晚 8 小時(shí)”這種尷尬。3. SpringBoot后端救助狀態(tài)機(jī)與小程序端API實(shí)現(xiàn)后端接口設(shè)計(jì)最怕兩種極端一種是按頁面拆接口比如 getIndexData、getDetailData另一種是每個(gè)表單獨(dú)暴露 CRUD小程序端可以直接改任意字段。正確做法是把接口按業(yè)務(wù)流程拆小程序端只能看到自己需要的操作比如提交領(lǐng)養(yǎng)申請、查詢申請記錄、上傳救助信息不能直接跨表改狀態(tài)。3.1 REST API 的路徑設(shè)計(jì)與統(tǒng)一返回體接口路徑建議這樣劃分方法路徑說明GET/api/animals?statusspeciespage1size10分頁查詢動(dòng)物列表GET/api/animals/{id}查詢動(dòng)物詳情POST/api/adopt/apply提交領(lǐng)養(yǎng)申請PUT/api/adopt/{id}/audit?passtrue管理員審核申請GET/api/rescue/records查詢救助記錄統(tǒng)一返回體使用一個(gè) Result 泛型類code 為 0 表示成功非 0 表示業(yè)務(wù)失敗。不要直接用 HTTP 狀態(tài)碼做業(yè)務(wù)判斷因?yàn)樾〕绦蚨撕秃蠖酥g的網(wǎng)關(guān)可能會重寫狀態(tài)碼而且 200 之外的響應(yīng)在 wx.request 里也能拿到但處理邏輯會碎。public class ResultT { private int code; private String msg; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 0; r.msg ok; r.data data; return r; } public static T ResultT error(int code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } }寫 Controller 的時(shí)候方法參數(shù)用 RequestParam 給默認(rèn)值分頁參數(shù) page 和 size 一定不能讓用戶傳負(fù)數(shù)。MyBatis-Plus 的 Page 對非法值會做修正但業(yè)務(wù)上最好在入口就擋住。GetMapping(/animals) public ResultPageAnimal list(RequestParam(defaultValue 1) long page, RequestParam(defaultValue 10) long size, RequestParam(required false) Integer status) { LambdaQueryWrapperAnimal wrapper Wrappers.lambdaQuery(); wrapper.eq(status ! null, Animal::getStatus, status) .orderByDesc(Animal::getCreateTime); return Result.ok(animalMapper.selectPage(new Page(page, size), wrapper)); }eq 方法第一個(gè)參數(shù)是布爾條件status 為 null 時(shí)不拼這個(gè)條件避免出現(xiàn)where status null。orderByDesc 直接用 Lambda 屬性引用不會因?yàn)樽侄胃拿└淖址?.2 領(lǐng)養(yǎng)申請的狀態(tài)流轉(zhuǎn)實(shí)現(xiàn)事務(wù)與行鎖審核領(lǐng)養(yǎng)申請是整個(gè)項(xiàng)目最核心的業(yè)務(wù)。很多人只寫了普通 select再 update并發(fā)時(shí)會出大問題。兩個(gè)管理員同時(shí)審核同一只動(dòng)物的不同申請都查到動(dòng)物狀態(tài)是待領(lǐng)養(yǎng)于是兩個(gè)都通過動(dòng)物就被“雙領(lǐng)養(yǎng)”了。解決辦法是先鎖行再判斷。Transactional(rollbackFor Exception.class) public void auditApply(Long applyId, boolean pass) { AdoptApplication apply adoptApplicationMapper.selectById(applyId); if (apply null || apply.getStatus() ! 0) { throw new BusinessException(申請不存在或已審核); } Animal animal animalMapper.selectByIdForUpdate(apply.getAnimalId()); if (animal null || animal.getStatus() ! 1) { throw new BusinessException(該動(dòng)物當(dāng)前不可領(lǐng)養(yǎng)); } if (pass) { animal.setStatus(2); animalMapper.updateById(animal); apply.setStatus(1); apply.setAuditTime(LocalDateTime.now()); adoptApplicationMapper.updateById(apply); LambdaUpdateWrapperAdoptApplication update Wrappers.lambdaUpdate(); update.eq(AdoptApplication::getAnimalId, apply.getAnimalId()) .eq(AdoptApplication::getStatus, 0) .ne(AdoptApplication::getId, applyId) .set(AdoptApplication::getStatus, 2); adoptApplicationMapper.update(null, update); } else { apply.setStatus(2); apply.setAuditTime(LocalDateTime.now()); adoptApplicationMapper.updateById(apply); } }selectByIdForUpdate 不是 MyBatis-Plus 自帶方法直接寫在 AnimalMapper 上即可Select(SELECT * FROM animal WHERE id #{id} FOR UPDATE) Animal selectByIdForUpdate(Param(id) Long id);這段邏輯有四個(gè)關(guān)鍵點(diǎn)。第一先鎖 animal 行再查申請和更新申請鎖順序固定避免兩個(gè)事務(wù)各自鎖了不同行然后互相等待的死鎖。第二FOR UPDATE會把 animal 這一行鎖到當(dāng)前事務(wù)提交第二個(gè)并發(fā)事務(wù)執(zhí)行同樣的 selectForUpdate 會阻塞直到第一個(gè)事務(wù)提交后重新讀取此時(shí) animal.status 已經(jīng)是已領(lǐng)養(yǎng)進(jìn)入“不可領(lǐng)養(yǎng)”分支。第三Transactional 默認(rèn)只對 RuntimeException 回滾這里指定 rollbackFor Exception.classBusinessException 也是繼承 RuntimeException所以業(yè)務(wù)異常同樣觸發(fā)回滾。第四批量把其他申請置為 2 時(shí)用 ne 排除當(dāng)前審核通過的這一條避免把自己的申請重新寫成已拒絕。3.2.1 為什么不直接用樂觀鎖樂觀鎖也可以做比如 animal 表加 version 字段update 時(shí)帶 version 條件影響行數(shù)為 0 就失敗。但這種情況需要前端重試或者后端循環(huán)重試邏輯分發(fā)到多個(gè)入口后很容易漏。單機(jī)部署的畢業(yè)設(shè)計(jì)悲觀鎖反而清晰鎖的那一行代碼就代表了“這只動(dòng)物同時(shí)只有一個(gè)審核事務(wù)能操作”。如果你面試被問到高并發(fā)可以補(bǔ)充說生產(chǎn)環(huán)境會把鎖上移到 Redis但演示時(shí)不用。3.3 SpringBoot配置、yml密文與上傳參數(shù)從 zip 里拿到項(xiàng)目后最常改的就是 application.yml。數(shù)據(jù)庫密碼、文件上傳大小、MyBatis-Plus 映射配置都在這一個(gè)文件里。建議密碼用 Jasypt 加密不要明文提交到壓縮包。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/animal_shelter?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ENC(這里放密文) servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jasypt: encryptor: password: ${JASYPT_PASSWORD}啟動(dòng)時(shí)指定環(huán)境變量java -jar app.jar --JASYPT_PASSWORD你的密鑰。這里的密碼不是數(shù)據(jù)庫密碼是 Jasypt 加密用的鹽配置文件里的 ENC() 密文必須用它才能解開。multipart 配置只控制 SpringBoot 接受的文件大小如果項(xiàng)目前端用 Nginx 轉(zhuǎn)發(fā)上傳請求Nginx 的 client_max_body_size 也要同步放開否則前端報(bào) 413但后端日志里什么都沒有。SpringBoot 版本如果特別新jasypt-spring-boot-starter 要用適配 SpringBoot 3 的分支否則啟動(dòng)會直接報(bào)自動(dòng)配置類不存在。這種問題很典型屬于“springboot版本太高”導(dǎo)致的依賴兼容性踩坑。不想引 Jasypt 的話另一個(gè)更簡單的方案是數(shù)據(jù)庫密碼不寫進(jìn) yml用環(huán)境變量password: ${DB_PASSWORD}部署時(shí)在系統(tǒng)環(huán)境變量里設(shè)置同樣能避免明文入庫。3.4 接口調(diào)試從Postman腳本到Burp Suite抓取小程序請求后端接口寫完先用 Postman 過一遍最基礎(chǔ)的 CRUD確認(rèn)返回結(jié)構(gòu)是 Result 的 JSON 格式。然后在小程序開發(fā)者工具里把 BASE_URL 指向本機(jī)局域網(wǎng) IP真機(jī)預(yù)覽時(shí)手機(jī)和電腦連同一個(gè) WiFi后端要在 application.yml 里加server.address0.0.0.0否則真機(jī)訪問不到。如果需要查看小程序?qū)嶋H發(fā)出去的請求體、響應(yīng)體、請求頭可以用 Burp Suite。常見做法是Burp Suite 里新建一個(gè)監(jiān)聽 127.0.0.1:8080 的項(xiàng)目微信開發(fā)者工具右上角“詳情 - 本地設(shè)置”里把請求流量指向這個(gè)地址再安裝并信任 Burp 的 CA 證書就能在 Burp 里看到 wx.request 從構(gòu)建請求到收到響應(yīng)的完整過程。這里要注意抓包工具只能幫你看請求不能幫你改數(shù)據(jù)庫調(diào)試狀態(tài)聯(lián)動(dòng)還是要在后端日志里打關(guān)鍵節(jié)點(diǎn)。4. 微信小程序端列表、詳情、狀態(tài)更新與表單校驗(yàn)小程序端更接近一個(gè)展示和錄入層業(yè)務(wù)規(guī)則盡量不放前端。列表頁從 /api/animals 拉數(shù)據(jù)詳情頁打開后展示動(dòng)物檔案和一個(gè)“申請領(lǐng)養(yǎng)”按鈕申請頁提交表單個(gè)人中心查看我的申請記錄和審核狀態(tài)。做到這四個(gè)頁面項(xiàng)目演示就已完整。4.1 小程序目錄結(jié)構(gòu)與導(dǎo)航欄適配小程序端目錄結(jié)構(gòu)建議這樣組織miniprogram/ app.js app.json pages/ index/ detail/ apply/ mine/ utils/ request.js images/app.json 里 pages 數(shù)組的第一項(xiàng)是啟動(dòng)頁。很多新手遇到每次編譯都進(jìn)錯(cuò)頁面問題就出在這里。要修改剛進(jìn)入的加載頁面直接調(diào)整 pages 數(shù)組順序而不是去 project.config.json 里找入口。頂部導(dǎo)航欄高度是小程序開發(fā)里的高頻問題。如果 app.json 里設(shè)置了自定義導(dǎo)航navigationStyle: custom頁面頂部狀態(tài)欄會空出一塊直接寫死 px 會在不同機(jī)型上錯(cuò)位。正確做法是拿系統(tǒng)狀態(tài)欄高度和膠囊按鈕位置計(jì)算function getNavInfo() { const windowInfo wx.getWindowInfo(); const menu wx.getMenuButtonBoundingClientRect(); const navBarHeight (menu.top - windowInfo.statusBarHeight) * 2 menu.height; return { statusBarHeight: windowInfo.statusBarHeight, navBarHeight: navBarHeight, menuHeight: menu.height }; }這個(gè)公式把膠囊按鈕到狀態(tài)欄的距離翻倍加上膠囊自身高度就是自定義導(dǎo)航欄的完整高度。用它去設(shè)置容器 paddingTop頁面內(nèi)容就不會被劉海屏和狀態(tài)欄遮擋。4.2 用Promise封裝wx.request并處理加載頁wx.request 默認(rèn)回調(diào)風(fēng)格頁面一多會出現(xiàn)多層嵌套。先封裝一層 Promise后續(xù)頁面統(tǒng)一調(diào) request 方法返回體已經(jīng)幫終端把 Result 拆好了。const BASE_URL http://192.168.1.100:8080/api; function request(url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}${url}, method: method, data: data, header: { Content-Type: application/json }, success: (res) { if (res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || 請求失敗, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 網(wǎng)絡(luò)異常, icon: none }); reject(err); } }); }); } module.exports { request };加載頁的體驗(yàn)問題是列表頁首次進(jìn)入容易白屏。原因很簡單onLoad 發(fā)請求是異步的頁面 onReady 先執(zhí)行數(shù)據(jù)回來后 setData 才觸發(fā)渲染。處理辦法是在 onLoad 里 wx.showLoading在請求結(jié)束后的 finally 里 wx.hideLoading。如果用了自定義導(dǎo)航欄wx.showNavigationBarLoading 不顯示所以不要依賴它。4.3 表單提交與radio-group單選框的坑申請頁的表單至少要包含姓名、電話、申請理由、居住情況。物種和性別篩選在列表頁用 radio-group 實(shí)現(xiàn)WXML 寫法radio-group bindchangeonSpeciesChange label wx:for{{speciesList}} wx:keyvalue classspecies-item radio value{{item.value}} checked{{item.checked}} color#07c160 / text{{item.label}}/text /label /radio-group對應(yīng)的 JSdata: { speciesList: [ { value: cat, label: 貓, checked: true }, { value: dog, label: 狗, checked: false } ] }, onSpeciesChange(e) { const selected e.detail.value; this.setData({ selectedSpecies: selected }); }這里有個(gè)典型坑radio 的 checked 必須是布爾值如果后端返回的 0/1 直接放進(jìn) WXML字符串false在部分基礎(chǔ)庫版本里會被當(dāng)作選中態(tài)導(dǎo)致兩個(gè)單選框同時(shí)高亮。正確做法是在接口返回后手動(dòng)轉(zhuǎn)換成布爾值。表單提交時(shí)手機(jī)號校驗(yàn)用正則const phoneReg /^1[3-9]\d{9}$/; if (!phone || !phoneReg.test(phone)) { wx.showToast({ title: 請輸入正確的手機(jī)號, icon: none }); return; }校驗(yàn)通過后再調(diào) request 提交提交按鈕加 disabled 狀態(tài)防止用戶連續(xù)點(diǎn)擊產(chǎn)生兩條申請這樣后端就算沒加防重前端也擋住了大部分重復(fù)請求。4.4 小程序與web-view內(nèi)嵌H5的頁面坑有的項(xiàng)目會把救助協(xié)議或多頁面的免責(zé)聲明用 web-view 內(nèi)嵌 H5。常見問題有兩個(gè)一個(gè)是“微信小程序內(nèi)嵌H5工具欄左側(cè)返回箭頭沒有了”還有一個(gè)是 H5 頁面內(nèi)跳轉(zhuǎn)后返回層級錯(cuò)亂。出現(xiàn)返回箭頭消失先檢查承載 web-view 的那個(gè)頁面的導(dǎo)航欄配置。如果這個(gè)頁面設(shè)置了自定義導(dǎo)航返回箭頭會被自己的自定義按鈕蓋住或覆蓋web-view 全屏?xí)r原本由宿主提供的導(dǎo)航 UI 就不穩(wěn)定。解決辦法是不對 web-view 頁面開自定義導(dǎo)航如果必須自定義就在 H5 頁面用 wx.miniProgram.postMessage 告訴小程序原生層手動(dòng)控制返回。另一個(gè)做法是 H5 內(nèi)部用 history.back() 自己維護(hù)返回而不依賴小程序的導(dǎo)航棧。web-view 和業(yè)務(wù)頁通信時(shí)建議把參數(shù)通過 URL 查詢串傳遞比如/pages/webview/webview?urlxxxanimalId1這樣 H5 側(cè)原生 JS 拿到的參數(shù)和后端日志里的請求參數(shù)能一一對上排查問題也容易。5. 用演示視頻反推驗(yàn)收清單從數(shù)據(jù)庫腳本到高分答辯一個(gè)高分項(xiàng)目的演示視頻不是隨便點(diǎn)兩下就行的它其實(shí)暴露了完整驗(yàn)收路徑。拿到 zip 后先看演示視頻里做了什么再反過來檢查自己能不能復(fù)現(xiàn)這個(gè)順序比直接看代碼更高效。5.1 演示視頻里必須出現(xiàn)的4個(gè)場景演示場景操作步驟答辯可說點(diǎn)首頁列表進(jìn)入小程序按狀態(tài)篩選動(dòng)物分頁、狀態(tài)過濾、數(shù)據(jù)庫索引提交領(lǐng)養(yǎng)申請打開詳情填寫表單提交參數(shù)校驗(yàn)、申請寫入審核通過后端或管理端調(diào)用審核接口行鎖、事務(wù)、狀態(tài)聯(lián)動(dòng)數(shù)據(jù)復(fù)查Navicat 查詢申請和動(dòng)物兩張表索引設(shè)計(jì)、邏輯外鍵取舍演示視頻里如果有管理端頁面那么管理端代碼也必須在 zip 中存在。很多資料包會混入兩個(gè)版本的后端演示視頻里調(diào)用的接口和當(dāng)前代碼對不上答辯時(shí)現(xiàn)場操作直接露餡。5.2 數(shù)據(jù)庫初始化腳本的三個(gè)檢查點(diǎn)先執(zhí)行命令導(dǎo)入數(shù)據(jù)庫mysql -u root -p animal_shelter init.sql如果導(dǎo)入報(bào)錯(cuò)優(yōu)先看三個(gè)檢查點(diǎn)。第一init.sql 里是否有 CREATE DATABASE 和 USE保證庫存在第二表是否都用了 InnoDB 和 utf8mb4整庫導(dǎo)出的腳本常見 DEFINER 綁定原用戶需要手動(dòng)去掉第三演示數(shù)據(jù)覆蓋 status 的 0、1、2 三種狀態(tài)沒有已領(lǐng)養(yǎng)數(shù)據(jù)就沒法演示審核通過后的聯(lián)動(dòng)效果。5.3 從zip到能run的最后一公里使用文檔如果照著做還是起不來大概率卡在三個(gè)本地差異application.yml 里的數(shù)據(jù)庫密碼不對、小程序 appid 不是自己的、BASE_URL 寫的 localhost。逐項(xiàng)改成自己的環(huán)境后后端執(zhí)行mvn spring-boot:run前端在微信開發(fā)者工具里導(dǎo)入項(xiàng)目本地開發(fā)時(shí)勾選“不校驗(yàn)合法域名”真機(jī)預(yù)覽再改成局域網(wǎng) IP。把這一套動(dòng)作在答辯前親手走一遍從數(shù)據(jù)庫腳本到演示視頻里最后一個(gè)點(diǎn)擊能全部復(fù)現(xiàn)項(xiàng)目基本不會卡在半路。本文還有配套的精品資源點(diǎn)擊獲取