約系統(tǒng)實(shí)戰(zhàn)開發(fā))
寵物美容預(yù)約是線下寵物門店非常典型的業(yè)務(wù)場(chǎng)景客戶需要提前挑選服務(wù)項(xiàng)目、選擇門店和美容師、確定到店時(shí)間門店則需要根據(jù)人力排班安排接待。傳統(tǒng)的人工預(yù)約方式容易出現(xiàn)信息遺漏、時(shí)間段沖突、客戶到店等待時(shí)間過長(zhǎng)等問題。本文以“基于微信小程序的寵物美容預(yù)約系統(tǒng)”為例從需求分析、架構(gòu)設(shè)計(jì)、數(shù)據(jù)庫設(shè)計(jì)、后端接口開發(fā)到小程序前端實(shí)現(xiàn)完整梳理一套可以實(shí)際運(yùn)行的預(yù)約系統(tǒng)開發(fā)流程。適合正在做微信小程序課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)或者想在真實(shí)項(xiàng)目中搭建預(yù)約類業(yè)務(wù)功能的開發(fā)者閱讀。整篇文章會(huì)圍繞“用戶端小程序 后端接口 數(shù)據(jù)庫”展開包含完整建表 SQL、Spring Boot 后端核心代碼、原生微信小程序前端代碼以及聯(lián)調(diào)部署階段的常見問題。你可以把它當(dāng)作一個(gè)可復(fù)用的預(yù)約系統(tǒng)最小閉環(huán)在此基礎(chǔ)上繼續(xù)擴(kuò)展商家端、支付、消息通知等能力。1. 項(xiàng)目背景與需求分析1.1 業(yè)務(wù)場(chǎng)景與痛點(diǎn)寵物美容服務(wù)和普通商品購買不同它強(qiáng)依賴“人”和“時(shí)間”。一位美容師同時(shí)段只能服務(wù)一到兩只寵物剪刀、吹水機(jī)、洗護(hù)臺(tái)都存在物理上限因此門店必須提前知道什么時(shí)間段有客戶來才能合理排班、準(zhǔn)備洗護(hù)物料。如果只靠微信群接龍、電話登記或到店排隊(duì)會(huì)出現(xiàn)幾個(gè)典型問題客戶和門店對(duì)時(shí)間理解不一致經(jīng)常出現(xiàn)“我約了下午兩點(diǎn)門店沒記錄”的扯皮。高峰期時(shí)段被重復(fù)占用門店無法直觀看到哪個(gè)時(shí)段已經(jīng)約滿。客戶信息、寵物信息每次都要手填體驗(yàn)差且容易記錯(cuò)品種、體重。預(yù)約記錄散落在聊天記錄里無法追溯也無法統(tǒng)計(jì)門店經(jīng)營(yíng)數(shù)據(jù)。預(yù)約系統(tǒng)的核心價(jià)值就是把“客戶想約的時(shí)間”和“門店可服務(wù)的時(shí)間”放到同一個(gè)透明池子里進(jìn)行匹配通過數(shù)據(jù)庫鎖和事務(wù)保證同一個(gè)時(shí)段不會(huì)被重復(fù)預(yù)約。1.2 功能模塊拆解面向普通用戶的微信小程序端主要包含以下功能模塊功能模塊說明微信登錄通過 wx.login 獲取 code后端調(diào)用微信接口換取 openid完成靜默注冊(cè)登錄首頁服務(wù)展示展示寵物美容服務(wù)項(xiàng)目包括洗澡、美容造型、SPA 等附帶價(jià)格和預(yù)計(jì)時(shí)長(zhǎng)寵物檔案管理用戶維護(hù)自己的寵物信息包括寵物名、類型、品種、體重、備注預(yù)約時(shí)直接選擇預(yù)約下單選擇寵物、服務(wù)項(xiàng)目、預(yù)約日期和時(shí)段生成預(yù)約訂單預(yù)約記錄查看歷史預(yù)約了解訂單狀態(tài)支持取消未開始的預(yù)約個(gè)人中心展示用戶信息、門店地址、聯(lián)系電話、營(yíng)業(yè)時(shí)間等商家端一般做成獨(dú)立的管理后臺(tái)或者商家版小程序本文重點(diǎn)介紹用戶端閉環(huán)同時(shí)會(huì)在后端保留商家核銷的狀態(tài)流轉(zhuǎn)邏輯方便后續(xù)擴(kuò)展。1.3 角色與核心業(yè)務(wù)流程系統(tǒng)涉及兩個(gè)核心角色普通用戶和門店管理員。核心預(yù)約流程如下用戶打開小程序自動(dòng)完成微信登錄。用戶在“寵物”頁維護(hù)寵物檔案首次使用需要先添加寵物。用戶在首頁選擇美容服務(wù)項(xiàng)目點(diǎn)擊“立即預(yù)約”。用戶選擇要服務(wù)的寵物、預(yù)約日期、可預(yù)約時(shí)段填寫備注。系統(tǒng)校驗(yàn)時(shí)段是否可約生成預(yù)約訂單并占用對(duì)應(yīng)時(shí)段名額。用戶到店后商家在管理端核銷訂單訂單狀態(tài)從“待服務(wù)”變?yōu)椤耙逊?wù)”。如果用戶無法到店可以在訂單開始前取消預(yù)約系統(tǒng)釋放時(shí)段名額。這里的難點(diǎn)在第 5 步多個(gè)用戶同時(shí)搶同一個(gè)時(shí)段時(shí)必須保證“時(shí)段名額扣減”和“訂單生成”是原子操作否則就會(huì)出現(xiàn)超賣。2. 系統(tǒng)架構(gòu)與技術(shù)選型2.1 技術(shù)棧說明本文選用的技術(shù)棧是原生微信小程序 Spring Boot MySQL這也是目前課程設(shè)計(jì)和中小型項(xiàng)目中比較主流、參考資料最多的一套組合。層級(jí)技術(shù)選型說明前端原生微信小程序WXML 負(fù)責(zé)頁面結(jié)構(gòu)WXSS 負(fù)責(zé)樣式JS 負(fù)責(zé)交互邏輯后端Spring Boot 2.x提供 RESTful 接口處理登錄、預(yù)約等業(yè)務(wù)ORMMyBatis-Plus簡(jiǎn)化單表 CRUD核心業(yè)務(wù)仍然使用自定義 SQL 控制并發(fā)數(shù)據(jù)庫MySQL 5.7 / 8.0存儲(chǔ)用戶、寵物、服務(wù)項(xiàng)目、時(shí)段、預(yù)約訂單數(shù)據(jù)本地聯(lián)調(diào)微信開發(fā)者工具 Postman小程序端聯(lián)調(diào)和后端接口測(cè)試如果你的項(xiàng)目更偏向輕量快速也可以把后端換成微信云開發(fā)直接用云函數(shù) 云數(shù)據(jù)庫實(shí)現(xiàn)。兩種方案各有優(yōu)劣云開發(fā)省去服務(wù)器運(yùn)維適合快速上線傳統(tǒng)后端更便于理解分層架構(gòu)適合學(xué)習(xí)框架設(shè)計(jì)和面試答辯。2.2 整體架構(gòu)與請(qǐng)求流程整個(gè)系統(tǒng)可以理解成三條鏈路登錄鏈路小程序wx.login拿到臨時(shí) code → 請(qǐng)求后端登錄接口 → 后端用 code 換取 openid → 查庫或注冊(cè)用戶 → 返回用戶信息。數(shù)據(jù)查詢鏈路小程序通過封裝好的 request 工具請(qǐng)求后端接口后端查詢數(shù)據(jù)庫返回統(tǒng)一格式 JSON。預(yù)約鏈路小程序提交預(yù)約參數(shù) → 后端校驗(yàn)寵物歸屬、服務(wù)狀態(tài)、時(shí)段余量 → 事務(wù)內(nèi)生成訂單并扣減時(shí)段名額 → 返回訂單號(hào)。后端接口統(tǒng)一返回{ code, message, data }結(jié)構(gòu)小程序端在 request 封裝里統(tǒng)一處理業(yè)務(wù)碼和網(wǎng)絡(luò)異常業(yè)務(wù)頁面只需要關(guān)注成功回調(diào)中的 data 即可。2.3 項(xiàng)目目錄結(jié)構(gòu)后端工程采用標(biāo)準(zhǔn) Spring Boot 分層結(jié)構(gòu)pet-grooming-server ├── pom.xml └── src/main/java/com/pet ├── PetGroomingApplication.java ├── common │ ├── Result.java │ └── ServiceException.java ├── config │ └── RestTemplateConfig.java ├── controller │ ├── UserController.java │ ├── ServiceController.java │ └── AppointmentController.java ├── entity │ ├── User.java │ ├── Pet.java │ ├── ServiceItem.java │ ├── AppointmentSlot.java │ └── Appointment.java ├── mapper │ ├── UserMapper.java │ ├── PetMapper.java │ ├── ServiceItemMapper.java │ ├── AppointmentSlotMapper.java │ └── AppointmentMapper.java └── service ├── UserService.java └── AppointmentService.java小程序端目錄結(jié)構(gòu)miniprogram ├── app.js ├── app.json ├── app.wxss ├── utils │ └── request.js └── pages ├── index // 首頁服務(wù)列表 ├── booking // 預(yù)約下單頁 ├── pet // 寵物檔案頁 ├── order // 預(yù)約記錄頁 └── profile // 個(gè)人中心3. 數(shù)據(jù)庫設(shè)計(jì)3.1 表關(guān)系概覽數(shù)據(jù)庫設(shè)計(jì)是預(yù)約系統(tǒng)的地基。本文設(shè)計(jì)五張核心表關(guān)系如下sys_user用戶表通過 openid 唯一標(biāo)識(shí)微信用戶。pet寵物表一個(gè)用戶可擁有多只寵物多對(duì)一關(guān)系。service_item服務(wù)項(xiàng)目表比如“小型犬洗澡”“貓咪美容”。appointment_slot預(yù)約時(shí)段表存儲(chǔ)某天某個(gè)時(shí)間段可預(yù)約數(shù)量。appointment預(yù)約訂單表關(guān)聯(lián)用戶、寵物、服務(wù)項(xiàng)目、時(shí)段。時(shí)段單獨(dú)建表而不是在訂單表里直接填一個(gè)時(shí)間字符串是因?yàn)轭A(yù)約系統(tǒng)需要“庫存”概念。每個(gè)時(shí)段有 total_count 和 booked_count 兩個(gè)字段下單時(shí)已經(jīng)約滿的時(shí)段不能繼續(xù)選擇。3.2 核心表字段設(shè)計(jì)要點(diǎn)用戶表使用openid作為唯一業(yè)務(wù)鍵不要用自增 id 作為微信登錄的判定條件因?yàn)橥挥脩糁匦率跈?quán)后 id 不會(huì)變但不同用戶不可能共享 openid。寵物表必須冗余user_id作為歸屬標(biāo)識(shí)后端在預(yù)約接口中要校驗(yàn)“這只寵物是否屬于當(dāng)前用戶”防止越權(quán)預(yù)約別人的寵物。預(yù)約訂單表使用獨(dú)立的order_no訂單號(hào)而不是依賴自增主鍵。訂單號(hào)可以用時(shí)間戳 隨機(jī)數(shù)生成也可以使用雪花算法方便后續(xù)對(duì)接物流、支付和對(duì)賬。時(shí)段表需要唯一索引(merchant_id, slot_date, start_time)從數(shù)據(jù)庫層面保證同一天同一個(gè)開始時(shí)間不會(huì)出現(xiàn)兩條重復(fù)配置。3.3 建表 SQL 與初始化數(shù)據(jù)CREATE DATABASE IF NOT EXISTS pet_grooming DEFAULT CHARACTER SET utf8mb4; USE pet_grooming; -- 用戶表 CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主鍵, openid VARCHAR(64) NOT NULL COMMENT 微信openid, nickname VARCHAR(50) DEFAULT COMMENT 昵稱, avatar_url VARCHAR(255) DEFAULT COMMENT 頭像, phone VARCHAR(20) DEFAULT COMMENT 手機(jī)號(hào), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB COMMENT用戶表; -- 寵物表 CREATE TABLE pet ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主鍵, user_id BIGINT NOT NULL COMMENT 所屬用戶ID, name VARCHAR(30) NOT NULL COMMENT 寵物名稱, category VARCHAR(20) NOT NULL COMMENT 寵物類型dog/cat/其他, breed VARCHAR(30) DEFAULT COMMENT 品種, age INT DEFAULT 0 COMMENT 年齡月, gender TINYINT DEFAULT 0 COMMENT 0未知 1公 2母, weight DECIMAL(5,2) DEFAULT 0 COMMENT 體重kg, remark VARCHAR(255) DEFAULT COMMENT 備注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB COMMENT寵物表; -- 服務(wù)項(xiàng)目表 CREATE TABLE service_item ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主鍵, merchant_id BIGINT NOT NULL DEFAULT 1 COMMENT 門店ID, name VARCHAR(50) NOT NULL COMMENT 服務(wù)名稱, description VARCHAR(255) DEFAULT COMMENT 服務(wù)說明, price DECIMAL(10,2) NOT NULL COMMENT 價(jià)格, duration INT NOT NULL DEFAULT 60 COMMENT 預(yù)計(jì)時(shí)長(zhǎng)分鐘, image VARCHAR(255) DEFAULT COMMENT 圖片, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB COMMENT服務(wù)項(xiàng)目表; -- 預(yù)約時(shí)段表 CREATE TABLE appointment_slot ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主鍵, merchant_id BIGINT NOT NULL COMMENT 門店ID, slot_date DATE NOT NULL COMMENT 日期, start_time VARCHAR(10) NOT NULL COMMENT 開始時(shí)間 HH:mm, end_time VARCHAR(10) NOT NULL COMMENT 結(jié)束時(shí)間 HH:mm, total_count INT DEFAULT 1 COMMENT 可預(yù)約數(shù)量, booked_count INT DEFAULT 0 COMMENT 已預(yù)約數(shù)量, status TINYINT DEFAULT 1 COMMENT 1可約 0關(guān)閉, PRIMARY KEY (id), UNIQUE KEY uk_slot (merchant_id, slot_date, start_time) ) ENGINEInnoDB COMMENT預(yù)約時(shí)段表; -- 預(yù)約訂單表 CREATE TABLE appointment ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主鍵, order_no VARCHAR(32) NOT NULL COMMENT 訂單編號(hào), user_id BIGINT NOT NULL COMMENT 用戶ID, pet_id BIGINT NOT NULL COMMENT 寵物ID, merchant_id BIGINT NOT NULL DEFAULT 1 COMMENT 門店ID, service_item_id BIGINT NOT NULL COMMENT 服務(wù)項(xiàng)目ID, slot_id BIGINT NOT NULL COMMENT 時(shí)段ID, appointment_date DATE NOT NULL COMMENT 預(yù)約日期, start_time VARCHAR(10) DEFAULT COMMENT 開始時(shí)間, end_time VARCHAR(10) DEFAULT COMMENT 結(jié)束時(shí)間, amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 金額, status TINYINT NOT NULL DEFAULT 1 COMMENT 1待服務(wù) 2已服務(wù) 3已取消, remark VARCHAR(255) DEFAULT COMMENT 用戶備注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB COMMENT預(yù)約訂單表;初始化幾條服務(wù)項(xiàng)目數(shù)據(jù)INSERT INTO service_item (merchant_id, name, description, price, duration) VALUES (1, 小型犬洗澡, 包含洗澡、吹干、耳部清潔, 68.00, 60), (1, 中型犬洗澡, 包含洗澡、吹干、耳部清潔, 88.00, 90), (1, 貓咪精致美容, 包含洗澡、吹干、修剪指甲, 108.00, 90), (1, 狗狗造型設(shè)計(jì), 包含洗澡、修剪造型、指甲修剪, 168.00, 120);初始化某一天的預(yù)約時(shí)段這里以 2025 年為例實(shí)際使用時(shí)應(yīng)由商家后臺(tái)按需生成INSERT INTO appointment_slot (merchant_id, slot_date, start_time, end_time, total_count, booked_count) VALUES (1, 2025-03-10, 09:00, 11:00, 2, 0), (1, 2025-03-10, 10:00, 12:00, 2, 0), (1, 2025-03-10, 13:00, 15:00, 2, 0), (1, 2025-03-10, 14:00, 16:00, 2, 0);需要說明的是slot_date和start_time只是示例數(shù)據(jù)你可以根據(jù)門店實(shí)際營(yíng)業(yè)時(shí)間生成后 7 天的時(shí)段。生產(chǎn)環(huán)境中通常用定時(shí)任務(wù)每天滾動(dòng)生成未來 N 天的時(shí)段避免人工維護(hù)。4. 環(huán)境準(zhǔn)備與項(xiàng)目初始化4.1 注冊(cè)小程序賬號(hào)與 AppID開發(fā)微信小程序之前需要到微信公眾平臺(tái)注冊(cè)一個(gè)小程序賬號(hào)類型選擇“個(gè)人”或“企業(yè)”都可以。注冊(cè)完成后在“開發(fā)管理 → 開發(fā)設(shè)置”頁面可以看到 AppID 和 AppSecret。這里有兩個(gè)注意事項(xiàng)個(gè)人主體的小程序部分能力受限但開發(fā)預(yù)約系統(tǒng)的基本功能沒有問題。AppSecret 非常敏感只能保存在后端服務(wù)器配置中絕不能寫在小程序前端代碼里否則任何人都能通過你的 AppSecret 獲取用戶信息。如果暫時(shí)不想注冊(cè)賬號(hào)也可以使用微信開發(fā)者工具里的“測(cè)試號(hào)”但測(cè)試號(hào)無法體驗(yàn)完整的登錄能力建議正式開發(fā)時(shí)使用真實(shí) AppID。4.2 安裝微信開發(fā)者工具從微信官方渠道下載穩(wěn)定版微信開發(fā)者工具安裝后使用小程序賬號(hào)掃碼登錄。創(chuàng)建項(xiàng)目時(shí)選擇“小程序”而不是“小游戲”后端服務(wù)選擇“不使用云服務(wù)”AppID 填入 4.1 節(jié)獲取的 ID。開發(fā)階段登錄接口需要真實(shí) AppID 和 AppSecret 才能換取 openid因此建議在application.yml中配置自己的參數(shù)。4.3 后端環(huán)境準(zhǔn)備后端需要準(zhǔn)備 JDK、Maven、MySQL。版本不需要完全固定以下組合在本文測(cè)試環(huán)境中可以正常工作實(shí)際開發(fā)時(shí)請(qǐng)以本機(jī)環(huán)境為準(zhǔn)JDK 1.8 或 11Maven 3.6MySQL 5.7 或 8.0IDEA 或 Eclipse先用 3.3 節(jié) SQL 在 MySQL 中建好數(shù)據(jù)庫和表。注意如果本機(jī) MySQL 密碼不是123456后續(xù)配置application.yml時(shí)要做對(duì)應(yīng)修改。5. 后端接口設(shè)計(jì)與實(shí)現(xiàn)5.1 項(xiàng)目依賴與基礎(chǔ)配置創(chuàng)建一個(gè) Spring Boot 工程pom.xml核心依賴如下。版本號(hào)建議以你創(chuàng)建工程時(shí)的 Spring Boot 版本為準(zhǔn)不要盲目復(fù)制。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies 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 groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesapplication.yml配置文件server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/pet_grooming?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/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 wechat: appid: 你的AppID secret: 你的AppSecretmap-underscore-to-camel-case開啟后數(shù)據(jù)庫字段user_id可以自動(dòng)映射到實(shí)體屬性u(píng)serId省去大量手動(dòng)映射。log-impl配置會(huì)在控制臺(tái)打印 SQL方便開發(fā)階段排查問題生產(chǎn)環(huán)境建議關(guān)閉或改為異步日志。5.2 統(tǒng)一返回結(jié)果封裝所有接口統(tǒng)一返回ResultT這樣前端只需要關(guān)注一種結(jié)構(gòu)也方便后續(xù)增加全局異常處理。// 文件路徑src/main/java/com/pet/common/Result.java package com.pet.common; import lombok.Data; Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }再定義一個(gè)簡(jiǎn)單的業(yè)務(wù)異常類方便在 Service 中主動(dòng)拋出錯(cuò)誤// 文件路徑src/main/java/com/pet/common/ServiceException.java package com.pet.common; public class ServiceException extends RuntimeException { public ServiceException(String message) { super(message); } }生產(chǎn)項(xiàng)目中還應(yīng)該配合RestControllerAdvice做統(tǒng)一異常處理避免異常堆棧直接暴露給前端。這里為了保持示例簡(jiǎn)潔只保留了最小閉環(huán)。5.3 微信登錄接口微信登錄流程是很多新手容易繞暈的地方。簡(jiǎn)單理解就是小程序端拿不到 openid只能通過wx.login拿到一個(gè)臨時(shí)憑證 code后端拿著 code 去微信服務(wù)器換 openid。code 有效期很短且只能使用一次所以不存在把 code 泄露給別人的風(fēng)險(xiǎn)。需要注意調(diào)用微信接口時(shí)需要配置 RestTemplate 這個(gè) Bean。先寫配置類// 文件路徑src/main/java/com/pet/config/RestTemplateConfig.java package com.pet.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; Configuration public class RestTemplateConfig { Bean public RestTemplate restTemplate() { return new RestTemplate(); } }UserService 實(shí)現(xiàn)// 文件路徑src/main/java/com/pet/service/impl/UserServiceImpl.java package com.pet.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.pet.common.ServiceException; import com.pet.entity.User; import com.pet.mapper.UserMapper; import com.pet.service.UserService; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; Service public class UserServiceImpl implements UserService { Resource private RestTemplate restTemplate; Resource private ObjectMapper objectMapper; Resource private UserMapper userMapper; Value(${wechat.appid}) private String appId; Value(${wechat.secret}) private String secret; Override Transactional(rollbackFor Exception.class) public User login(String code) throws Exception { String url https://api.weixin.qq.com/sns/jscode2session ?appid appId secret secret js_code code grant_typeauthorization_code; String response restTemplate.getForObject(url, String.class); JsonNode node objectMapper.readTree(response); String openid node.path(openid).asText(); if (openid.isEmpty()) { throw new ServiceException(微信登錄失敗請(qǐng)檢查 appid 和 secret 配置); } User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getOpenid, openid) ); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(微信用戶); userMapper.insert(user); } return user; } }這里要重點(diǎn)解釋一下為什么用openid而不是自增 id 判斷老用戶每個(gè)微信用戶在小程序下的 openid 是唯一的用戶刪除小程序后再次進(jìn)入openid 也不會(huì)變因此 openid 天然可以作為業(yè)務(wù)主鍵。自增 id 只是數(shù)據(jù)庫內(nèi)部主鍵不能用于識(shí)別用戶身份。Transactional保證查詢和插入的原子性。生產(chǎn)項(xiàng)目中登錄成功后建議生成自己的 token 返回給前端后續(xù)請(qǐng)求攜帶 token服務(wù)端通過 token 識(shí)別用戶而不是每次都把 userId 放在請(qǐng)求參數(shù)里。本文示例為了展示業(yè)務(wù)主流程簡(jiǎn)化了這一步在第 9 節(jié)會(huì)專門強(qiáng)調(diào)安全改造方案。5.4 服務(wù)項(xiàng)目接口服務(wù)項(xiàng)目接口比較簡(jiǎn)單查詢所有上架項(xiàng)目即可// 文件路徑src/main/java/com/pet/controller/ServiceController.java package com.pet.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.pet.common.Result; import com.pet.entity.ServiceItem; import com.pet.mapper.ServiceItemMapper; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.List; RestController RequestMapping(/api/service) public class ServiceController { Resource private ServiceItemMapper serviceItemMapper; GetMapping(/list) public ResultListServiceItem list() { ListServiceItem list serviceItemMapper.selectList( new LambdaQueryWrapperServiceItem() .eq(ServiceItem::getStatus, 1) .orderByAsc(ServiceItem::getId) ); return Result.success(list); } }LambdaQueryWrapper是 MyBatis-Plus 提供的類型安全查詢構(gòu)造器使用實(shí)體方法引用而不是字符串字段名編譯期就能發(fā)現(xiàn)字段拼寫錯(cuò)誤。這個(gè)接口返回的數(shù)據(jù)會(huì)直接渲染到小程序首頁。5.5 預(yù)約下單接口與并發(fā)控制預(yù)約下單是整個(gè)系統(tǒng)的核心接口也是技術(shù)上最有含量的一塊。先看業(yè)務(wù)校驗(yàn)順序校驗(yàn)寵物是否存在且屬于當(dāng)前用戶。校驗(yàn)服務(wù)項(xiàng)目是否存在且已上架。鎖定預(yù)約時(shí)段判斷余量。生成訂單扣減時(shí)段余量。第三步需要特別處理。如果兩個(gè)用戶同時(shí)預(yù)約同一個(gè)時(shí)段直接采用“先查 booked_count 再 update”的方式可能兩個(gè)請(qǐng)求都讀到剩余 1結(jié)果兩個(gè)訂單都成功數(shù)據(jù)上就出現(xiàn)了超賣。解決方式是在事務(wù)中給時(shí)段記錄加行鎖。AppointmentSlotMapper 中定義如下方法// 文件路徑src/main/java/com/pet/mapper/AppointmentSlotMapper.java package com.pet.mapper; import com.baomidou.mybatisplus.core.mapper