ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于微信小程序与Spring Boot的宠物美容预约系统实战开发

基于微信小程序与Spring Boot的宠物美容预约系统实战开发 宠物美容预约是线下宠物门店非常典型的业务场景客户需要提前挑选服务项目、选择门店和美容师、确定到店时间门店则需要根据人力排班安排接待。传统的人工预约方式容易出现信息遗漏、时间段冲突、客户到店等待时间过长等问题。本文以“基于微信小程序的宠物美容预约系统”为例从需求分析、架构设计、数据库设计、后端接口开发到小程序前端实现完整梳理一套可以实际运行的预约系统开发流程。适合正在做微信小程序课程设计、毕业设计或者想在真实项目中搭建预约类业务功能的开发者阅读。整篇文章会围绕“用户端小程序 后端接口 数据库”展开包含完整建表 SQL、Spring Boot 后端核心代码、原生微信小程序前端代码以及联调部署阶段的常见问题。你可以把它当作一个可复用的预约系统最小闭环在此基础上继续扩展商家端、支付、消息通知等能力。1. 项目背景与需求分析1.1 业务场景与痛点宠物美容服务和普通商品购买不同它强依赖“人”和“时间”。一位美容师同时段只能服务一到两只宠物剪刀、吹水机、洗护台都存在物理上限因此门店必须提前知道什么时间段有客户来才能合理排班、准备洗护物料。如果只靠微信群接龙、电话登记或到店排队会出现几个典型问题客户和门店对时间理解不一致经常出现“我约了下午两点门店没记录”的扯皮。高峰期时段被重复占用门店无法直观看到哪个时段已经约满。客户信息、宠物信息每次都要手填体验差且容易记错品种、体重。预约记录散落在聊天记录里无法追溯也无法统计门店经营数据。预约系统的核心价值就是把“客户想约的时间”和“门店可服务的时间”放到同一个透明池子里进行匹配通过数据库锁和事务保证同一个时段不会被重复预约。1.2 功能模块拆解面向普通用户的微信小程序端主要包含以下功能模块功能模块说明微信登录通过 wx.login 获取 code后端调用微信接口换取 openid完成静默注册登录首页服务展示展示宠物美容服务项目包括洗澡、美容造型、SPA 等附带价格和预计时长宠物档案管理用户维护自己的宠物信息包括宠物名、类型、品种、体重、备注预约时直接选择预约下单选择宠物、服务项目、预约日期和时段生成预约订单预约记录查看历史预约了解订单状态支持取消未开始的预约个人中心展示用户信息、门店地址、联系电话、营业时间等商家端一般做成独立的管理后台或者商家版小程序本文重点介绍用户端闭环同时会在后端保留商家核销的状态流转逻辑方便后续扩展。1.3 角色与核心业务流程系统涉及两个核心角色普通用户和门店管理员。核心预约流程如下用户打开小程序自动完成微信登录。用户在“宠物”页维护宠物档案首次使用需要先添加宠物。用户在首页选择美容服务项目点击“立即预约”。用户选择要服务的宠物、预约日期、可预约时段填写备注。系统校验时段是否可约生成预约订单并占用对应时段名额。用户到店后商家在管理端核销订单订单状态从“待服务”变为“已服务”。如果用户无法到店可以在订单开始前取消预约系统释放时段名额。这里的难点在第 5 步多个用户同时抢同一个时段时必须保证“时段名额扣减”和“订单生成”是原子操作否则就会出现超卖。2. 系统架构与技术选型2.1 技术栈说明本文选用的技术栈是原生微信小程序 Spring Boot MySQL这也是目前课程设计和中小型项目中比较主流、参考资料最多的一套组合。层级技术选型说明前端原生微信小程序WXML 负责页面结构WXSS 负责样式JS 负责交互逻辑后端Spring Boot 2.x提供 RESTful 接口处理登录、预约等业务ORMMyBatis-Plus简化单表 CRUD核心业务仍然使用自定义 SQL 控制并发数据库MySQL 5.7 / 8.0存储用户、宠物、服务项目、时段、预约订单数据本地联调微信开发者工具 Postman小程序端联调和后端接口测试如果你的项目更偏向轻量快速也可以把后端换成微信云开发直接用云函数 云数据库实现。两种方案各有优劣云开发省去服务器运维适合快速上线传统后端更便于理解分层架构适合学习框架设计和面试答辩。2.2 整体架构与请求流程整个系统可以理解成三条链路登录链路小程序wx.login拿到临时 code → 请求后端登录接口 → 后端用 code 换取 openid → 查库或注册用户 → 返回用户信息。数据查询链路小程序通过封装好的 request 工具请求后端接口后端查询数据库返回统一格式 JSON。预约链路小程序提交预约参数 → 后端校验宠物归属、服务状态、时段余量 → 事务内生成订单并扣减时段名额 → 返回订单号。后端接口统一返回{ code, message, data }结构小程序端在 request 封装里统一处理业务码和网络异常业务页面只需要关注成功回调中的 data 即可。2.3 项目目录结构后端工程采用标准 Spring Boot 分层结构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小程序端目录结构miniprogram ├── app.js ├── app.json ├── app.wxss ├── utils │ └── request.js └── pages ├── index // 首页服务列表 ├── booking // 预约下单页 ├── pet // 宠物档案页 ├── order // 预约记录页 └── profile // 个人中心3. 数据库设计3.1 表关系概览数据库设计是预约系统的地基。本文设计五张核心表关系如下sys_user用户表通过 openid 唯一标识微信用户。pet宠物表一个用户可拥有多只宠物多对一关系。service_item服务项目表比如“小型犬洗澡”“猫咪美容”。appointment_slot预约时段表存储某天某个时间段可预约数量。appointment预约订单表关联用户、宠物、服务项目、时段。时段单独建表而不是在订单表里直接填一个时间字符串是因为预约系统需要“库存”概念。每个时段有 total_count 和 booked_count 两个字段下单时已经约满的时段不能继续选择。3.2 核心表字段设计要点用户表使用openid作为唯一业务键不要用自增 id 作为微信登录的判定条件因为同一用户重新授权后 id 不会变但不同用户不可能共享 openid。宠物表必须冗余user_id作为归属标识后端在预约接口中要校验“这只宠物是否属于当前用户”防止越权预约别人的宠物。预约订单表使用独立的order_no订单号而不是依赖自增主键。订单号可以用时间戳 随机数生成也可以使用雪花算法方便后续对接物流、支付和对账。时段表需要唯一索引(merchant_id, slot_date, start_time)从数据库层面保证同一天同一个开始时间不会出现两条重复配置。3.3 建表 SQL 与初始化数据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 手机号, 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宠物表; -- 服务项目表 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 服务名称, description VARCHAR(255) DEFAULT COMMENT 服务说明, price DECIMAL(10,2) NOT NULL COMMENT 价格, duration INT NOT NULL DEFAULT 60 COMMENT 预计时长分钟, 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服务项目表; -- 预约时段表 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 开始时间 HH:mm, end_time VARCHAR(10) NOT NULL COMMENT 结束时间 HH:mm, total_count INT DEFAULT 1 COMMENT 可预约数量, booked_count INT DEFAULT 0 COMMENT 已预约数量, status TINYINT DEFAULT 1 COMMENT 1可约 0关闭, PRIMARY KEY (id), UNIQUE KEY uk_slot (merchant_id, slot_date, start_time) ) ENGINEInnoDB COMMENT预约时段表; -- 预约订单表 CREATE TABLE appointment ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, 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 服务项目ID, slot_id BIGINT NOT NULL COMMENT 时段ID, appointment_date DATE NOT NULL COMMENT 预约日期, start_time VARCHAR(10) DEFAULT COMMENT 开始时间, end_time VARCHAR(10) DEFAULT COMMENT 结束时间, amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 金额, status TINYINT NOT NULL DEFAULT 1 COMMENT 1待服务 2已服务 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预约订单表;初始化几条服务项目数据INSERT INTO service_item (merchant_id, name, description, price, duration) VALUES (1, 小型犬洗澡, 包含洗澡、吹干、耳部清洁, 68.00, 60), (1, 中型犬洗澡, 包含洗澡、吹干、耳部清洁, 88.00, 90), (1, 猫咪精致美容, 包含洗澡、吹干、修剪指甲, 108.00, 90), (1, 狗狗造型设计, 包含洗澡、修剪造型、指甲修剪, 168.00, 120);初始化某一天的预约时段这里以 2025 年为例实际使用时应由商家后台按需生成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只是示例数据你可以根据门店实际营业时间生成后 7 天的时段。生产环境中通常用定时任务每天滚动生成未来 N 天的时段避免人工维护。4. 环境准备与项目初始化4.1 注册小程序账号与 AppID开发微信小程序之前需要到微信公众平台注册一个小程序账号类型选择“个人”或“企业”都可以。注册完成后在“开发管理 → 开发设置”页面可以看到 AppID 和 AppSecret。这里有两个注意事项个人主体的小程序部分能力受限但开发预约系统的基本功能没有问题。AppSecret 非常敏感只能保存在后端服务器配置中绝不能写在小程序前端代码里否则任何人都能通过你的 AppSecret 获取用户信息。如果暂时不想注册账号也可以使用微信开发者工具里的“测试号”但测试号无法体验完整的登录能力建议正式开发时使用真实 AppID。4.2 安装微信开发者工具从微信官方渠道下载稳定版微信开发者工具安装后使用小程序账号扫码登录。创建项目时选择“小程序”而不是“小游戏”后端服务选择“不使用云服务”AppID 填入 4.1 节获取的 ID。开发阶段登录接口需要真实 AppID 和 AppSecret 才能换取 openid因此建议在application.yml中配置自己的参数。4.3 后端环境准备后端需要准备 JDK、Maven、MySQL。版本不需要完全固定以下组合在本文测试环境中可以正常工作实际开发时请以本机环境为准JDK 1.8 或 11Maven 3.6MySQL 5.7 或 8.0IDEA 或 Eclipse先用 3.3 节 SQL 在 MySQL 中建好数据库和表。注意如果本机 MySQL 密码不是123456后续配置application.yml时要做对应修改。5. 后端接口设计与实现5.1 项目依赖与基础配置创建一个 Spring Boot 工程pom.xml核心依赖如下。版本号建议以你创建工程时的 Spring Boot 版本为准不要盲目复制。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开启后数据库字段user_id可以自动映射到实体属性userId省去大量手动映射。log-impl配置会在控制台打印 SQL方便开发阶段排查问题生产环境建议关闭或改为异步日志。5.2 统一返回结果封装所有接口统一返回ResultT这样前端只需要关注一种结构也方便后续增加全局异常处理。// 文件路径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; } }再定义一个简单的业务异常类方便在 Service 中主动抛出错误// 文件路径src/main/java/com/pet/common/ServiceException.java package com.pet.common; public class ServiceException extends RuntimeException { public ServiceException(String message) { super(message); } }生产项目中还应该配合RestControllerAdvice做统一异常处理避免异常堆栈直接暴露给前端。这里为了保持示例简洁只保留了最小闭环。5.3 微信登录接口微信登录流程是很多新手容易绕晕的地方。简单理解就是小程序端拿不到 openid只能通过wx.login拿到一个临时凭证 code后端拿着 code 去微信服务器换 openid。code 有效期很短且只能使用一次所以不存在把 code 泄露给别人的风险。需要注意调用微信接口时需要配置 RestTemplate 这个 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 实现// 文件路径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(微信登录失败请检查 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; } }这里要重点解释一下为什么用openid而不是自增 id 判断老用户每个微信用户在小程序下的 openid 是唯一的用户删除小程序后再次进入openid 也不会变因此 openid 天然可以作为业务主键。自增 id 只是数据库内部主键不能用于识别用户身份。Transactional保证查询和插入的原子性。生产项目中登录成功后建议生成自己的 token 返回给前端后续请求携带 token服务端通过 token 识别用户而不是每次都把 userId 放在请求参数里。本文示例为了展示业务主流程简化了这一步在第 9 节会专门强调安全改造方案。5.4 服务项目接口服务项目接口比较简单查询所有上架项目即可// 文件路径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 提供的类型安全查询构造器使用实体方法引用而不是字符串字段名编译期就能发现字段拼写错误。这个接口返回的数据会直接渲染到小程序首页。5.5 预约下单接口与并发控制预约下单是整个系统的核心接口也是技术上最有含量的一块。先看业务校验顺序校验宠物是否存在且属于当前用户。校验服务项目是否存在且已上架。锁定预约时段判断余量。生成订单扣减时段余量。第三步需要特别处理。如果两个用户同时预约同一个时段直接采用“先查 booked_count 再 update”的方式可能两个请求都读到剩余 1结果两个订单都成功数据上就出现了超卖。解决方式是在事务中给时段记录加行锁。AppointmentSlotMapper 中定义如下方法// 文件路径src/main/java/com/pet/mapper/AppointmentSlotMapper.java package com.pet.mapper; import com.baomidou.mybatisplus.core.mapper
返回列表