尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于微信小程序的宠物美容预约系统设计与实现

基于微信小程序的宠物美容预约系统设计与实现 最近在做一个宠物美容预约类的小程序项目从需求拆解、界面设计到后端联调踩了不少坑。网上关于“宠物美容预约系统”的资料虽然不少但大多是零散的功能截图或者不完整的代码片段真正能照着做下来的完整教程很少。这篇文章就围绕“基于微信小程序的宠物美容预约系统”这个主题整理一套可以落地的设计与实现方案包含需求分析、数据库设计、小程序前端核心代码、后端接口设计、常见报错排查和工程化建议。不管你是准备做毕业设计还是接了一个真实的小程序预约单子都可以参考这套思路。1. 项目背景与核心功能定位1.1 为什么选择微信小程序做宠物美容预约宠物美容预约本质上是一个“高频、低价、强到店”的本地生活服务场景。用户不需要下载独立 App通过微信扫一扫或者搜索小程序就能完成预约、查看订单、取消预约等操作这正好符合微信小程序“即用即走”的轻量特点。从商家角度看预约系统需要解决三个核心问题避免集中到店导致的拥挤合理分配美容师的时间。减少电话预约和手工记账带来的错单、漏单。积累用户和宠物档案便于后续营销和复购。所以这个系统并不是做一个简单的“表单提交页”而是需要覆盖用户端、商家端两条业务链路。1.2 系统核心角色与功能拆解在这个项目中我会把系统分为三个角色普通用户、商家管理员、系统后台。每个角色的功能边界如下角色主要功能普通用户微信授权登录、浏览服务项目、查看美容师、选择门店、提交预约、查看/取消订单、宠物档案管理商家管理员预约单审核、服务项目上下架、美容师排班、门店信息维护、订单统计系统后台用户管理、订单数据统计、优惠券配置、系统参数设置对于大多数课设或小型商用项目第一版不建议把后台做得太重优先把“用户登录 → 浏览服务 → 选择时间 → 提交预约 → 商家接单 → 用户查看订单”这条主链路跑通。1.3 技术选型说明本项目采用前后端分离思路前端微信小程序原生框架使用 WXML、WXSS、JavaScript。后端可以使用微信云开发也可以使用自建后端Spring Boot MySQL。两者各有优劣势。方案优势劣势微信云开发无需自建服务器自带数据库、云函数、存储适合快速上线冷启动稍慢部分复杂查询较弱自建后端接口灵活可控适合复杂业务方便与现有系统集成需要购买服务器需要配置域名与 HTTPS本文以“原生小程序 后端接口”的方式为例因为这种方式在订单、支付、权限等业务扩展上更接近真实项目。如果你只是为了课程设计演示也可以把后端换成微信云开发云函数整体思路不变。2. 系统需求分析与数据库设计2.1 功能需求细节在写代码之前先把需求拆细避免开发过程中反复改表。一个比较合理的预约流程如下用户打开小程序通过微信授权获取 openid。用户为宠物创建档案填写宠物昵称、品种、体重、过敏史等信息。用户浏览美容服务项目例如“小型犬精洗”“猫咪药浴”“造型修剪”等。用户选择门店、美容师、预约日期和时段。用户提交预约生成待确认订单。商家在管理端确认预约或者根据实际情况改期、取消。用户在小程序“我的预约”中查看订单状态。这里要注意预约订单存在“状态流转”不要把订单状态只设计成一个固定的值需要预留状态机。2.2 数据库表结构设计虽然微信小程序云开发可以使用 JSON 文档型数据库但从教学和工程规范化角度这里给出 MySQL 的建表 SQL方便你改造成其他数据库。-- 用户表 CREATE TABLE t_user ( id bigint(20) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信openid, nickname varchar(64) DEFAULT COMMENT 昵称, avatar varchar(255) DEFAULT COMMENT 头像, phone varchar(20) DEFAULT COMMENT 手机号, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 宠物档案表 CREATE TABLE t_pet ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 所属用户, pet_name varchar(32) NOT NULL COMMENT 宠物昵称, pet_type varchar(16) DEFAULT dog COMMENT 宠物类型 dog/cat, breed varchar(32) DEFAULT COMMENT 品种, weight decimal(5,2) DEFAULT NULL COMMENT 体重kg, allergy_info varchar(255) DEFAULT COMMENT 过敏史, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物档案表; -- 服务项目表 CREATE TABLE t_service ( id bigint(20) NOT NULL AUTO_INCREMENT, service_name varchar(64) NOT NULL COMMENT 服务名称, service_desc varchar(255) DEFAULT COMMENT 服务描述, price decimal(10,2) NOT NULL COMMENT 价格, duration_minutes int(11) DEFAULT 60 COMMENT 预计耗时, status tinyint(4) DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT服务项目表; -- 美容师表 CREATE TABLE t_staff ( id bigint(20) NOT NULL AUTO_INCREMENT, staff_name varchar(32) NOT NULL COMMENT 美容师姓名, avatar varchar(255) DEFAULT COMMENT 头像, intro varchar(255) DEFAULT COMMENT 个人简介, service_ids varchar(128) DEFAULT COMMENT 可提供服务id逗号分隔, status tinyint(4) DEFAULT 1 COMMENT 1在职 0离职, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT美容师表; -- 预约订单表 CREATE TABLE t_appointment ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL COMMENT 用户id, pet_id bigint(20) NOT NULL COMMENT 宠物id, service_id bigint(20) NOT NULL COMMENT 服务项目id, staff_id bigint(20) DEFAULT NULL COMMENT 美容师id, appointment_date date NOT NULL COMMENT 预约日期, time_slot varchar(16) NOT NULL COMMENT 时段 如 09:00-10:00, remark varchar(255) DEFAULT COMMENT 用户备注, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_date_status (appointment_date, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约订单表;2.3 数据库设计的几个注意点预约订单需要同时查用户和时间维度所以idx_user_id和idx_date_status两个索引比较重要。宠物档案和服务项目用独立的表避免将来扩展时改主表结构。状态字段用tinyint而不是字符串能减少无效数据并提升查询性能。如果涉及支付需要额外增加“支付流水表”和“退款表”这里先不做展开。3. 微信小程序前端环境准备与项目结构3.1 注册小程序账号与获取 AppID开发微信小程序前你需要先到微信公众平台注册一个小程序账号。注册成功后在“开发管理 → 开发设置”中可以看到 AppID 和 AppSecret。提示AppSecret 不要明文写在小程序代码中尤其是提交代码时不要把密钥传到 Git 仓库。正常项目中应该在服务端保存 AppSecret。3.2 创建原生小程序项目结构在微信开发者工具中新建项目语言选择 JavaScript框架选择原生小程序。建议的项目结构如下├── app.js // 小程序入口逻辑 ├── app.json // 全局配置 ├── app.wxss // 全局样式 ├── project.config.json // 开发者工具项目配置 ├── utils/ │ ├── request.js // 封装 wx.request 请求 │ └── auth.js // 登录态管理 ├── pages/ │ ├── index/ // 首页服务列表 │ ├── appointment/ // 预约下单页 │ ├── order/ // 订单列表 │ ├── order-detail/ // 订单详情 │ ├── pet/ // 宠物档案 │ └── mine/ // 个人中心 └── images/ // 本地静态图片3.3 全局配置 app.json在app.json中需要注册所有页面路径并配置窗口样式。一个典型的配置如下{ pages: [ pages/index/index, pages/appointment/appointment, pages/order/order, pages/order-detail/order-detail, pages/pet/pet, pages/mine/mine ], window: { backgroundTextStyle: light, navigationBarBackgroundColor: #ff8a5c, navigationBarTitleText: 宠物美容预约, navigationBarTextStyle: white }, tabBar: { color: #999999, selectedColor: #ff8a5c, list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/order/order, text: 预约 }, { pagePath: pages/mine/mine, text: 我的 } ] }, style: v2, sitemapLocation: sitemap.json }这里把预约入口放在“首页”用户最终通过首页选择服务进入预约页。tabBar的三个入口覆盖了用户最常用的三个页面浏览、预约、个人中心。3.4 封装 request 请求工具小程序中所有接口请求都建议统一走一个封装好的wx.request这样便于统一处理登录态、错误提示和 loading 效果。// 文件路径utils/request.js const BASE_URL https://your-domain.com/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, Authorization: wx.getStorageSync(token) || }, success: (res) { if (res.statusCode 200 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: 网络异常, icon: none }); reject(err); } }); }); } module.exports { get: (url, data) request(url, GET, data), post: (url, data) request(url, POST, data) };这里约定的后端返回结构是{ code: 0, msg: success, data: {} }code为 0 表示成功非 0 表示业务错误。这种方式在前后端分离项目里很常见能减少重复的响应判断代码。4. 完整实战案例预约功能从服务列表到下单下面通过一个完整流程演示从首页服务列表、预约页面到提交订单的实现过程。4.1 首页服务列表首页pages/index/index.wxml用来展示服务项目和美容师入口。为了不引入额外的 UI 组件库这里用最简单的卡片布局实现。!-- 文件路径pages/index/index.wxml -- view classcontainer view classbanner text classbanner-title宠物美容预约/text text classbanner-sub专业美容师贴心服务/text /view view classsection-title热门服务/view view classservice-list view classservice-card wx:for{{serviceList}} wx:keyid bindtapgoAppointment >// 文件路径pages/index/index.js const { get } require(../../utils/request); Page({ data: { serviceList: [] }, onLoad() { this.loadServices(); }, async loadServices() { try { const res await get(/service/list); this.setData({ serviceList: res }); } catch (e) { console.error(加载服务列表失败, e); } }, goAppointment(e) { const id e.currentTarget.dataset.id; wx.navigateTo({ url: /pages/appointment/appointment?serviceId${id} }); } });首页的核心逻辑很简单调用后端/service/list获取上架的服务项目渲染成卡片列表。用户点击卡片时把serviceId通过 URL 参数带到预约页。4.2 预约页面实现预约页面是系统中业务逻辑最复杂的一页需要展示用户选择的宠物、服务时段、美容师等信息。!-- 文件路径pages/appointment/appointment.wxml -- view classcontainer view classcard view classcard-title选择宠物/view picker range{{petList}} range-keypetName bindchangeonPetChange view classpicker-value{{currentPet.petName || 请选择宠物}}/view /picker /view view classcard view classcard-title服务项目/view view classservice-row text{{service.serviceName}}/text text classservice-price¥{{service.price}}/text /view /view view classcard view classcard-title选择日期/view picker modedate start{{today}} bindchangeonDateChange view classpicker-value{{appointmentDate || 请选择日期}}/view /picker /view view classcard view classcard-title选择时段/view view classslot-list view classslot-item {{selectedSlot item ? active : }} wx:for{{slotList}} wx:key*this bindtaponSlotChange >// 文件路径pages/appointment/appointment.js const { get, post } require(../../utils/request); Page({ data: { serviceId: null, service: {}, petList: [], currentPet: {}, staffList: [], slotList: [09:00-10:00, 10:00-11:00, 14:00-15:00, 15:00-16:00], appointmentDate: , selectedSlot: , selectedStaffId: null, remark: , today: }, onLoad(options) { const serviceId Number(options.serviceId); this.setData({ serviceId, today: this.formatDate(new Date()) }); this.loadServiceDetail(serviceId); this.loadPetList(); }, formatDate(date) { const y date.getFullYear(); const m String(date.getMonth() 1).padStart(2, 0); const d String(date.getDate()).padStart(2, 0); return ${y}-${m}-${d}; }, async loadServiceDetail(serviceId) { try { const res await get(/service/detail, { id: serviceId }); this.setData({ service: res }); this.loadStaffList(res.id); } catch (e) { console.error(加载服务详情失败, e); } }, async loadPetList() { try { const res await get(/pet/list); this.setData({ petList: res, currentPet: res.length 0 ? res[0] : {} }); } catch (e) { console.error(加载宠物列表失败, e); } }, async loadStaffList(serviceId) { try { const res await get(/staff/list, { serviceId }); this.setData({ staffList: res }); } catch (e) { console.error(加载美容师列表失败, e); } }, onPetChange(e) { const index Number(e.detail.value); this.setData({ currentPet: this.data.petList[index] }); }, onDateChange(e) { this.setData({ appointmentDate: e.detail.value }); }, onSlotChange(e) { this.setData({ selectedSlot: e.currentTarget.dataset.slot }); }, onStaffChange(e) { this.setData({ selectedStaffId: Number(e.currentTarget.dataset.id) }); }, onRemarkInput(e) { this.setData({ remark: e.detail.value }); }, async submitAppointment() { const { serviceId, currentPet, service, appointmentDate, selectedSlot, selectedStaffId, remark } this.data; if (!currentPet.id) { wx.showToast({ title: 请先添加宠物, icon: none }); return; } if (!appointmentDate) { wx.showToast({ title: 请选择预约日期, icon: none }); return; } if (!selectedSlot) { wx.showToast({ title: 请选择预约时段, icon: none }); return; } if (!selectedStaffId) { wx.showToast({ title: 请选择美容师, icon: none }); return; } try { await post(/appointment/create, { serviceId, petId: currentPet.id, appointmentDate, timeSlot: selectedSlot, staffId: selectedStaffId, remark }); wx.showToast({ title: 预约成功, icon: success }); setTimeout(() { wx.redirectTo({ url: /pages/order/order }); }, 1500); } catch (e) { console.error(提交预约失败, e); } } });需要注意预约页面在提交前做了必要字段校验避免把脏数据传给后端。实际项目中还应该加入“日期不能为过去日期”“时段已经被约满”等校验这些建议放在后端做前端校验只能作为体验优化。4.3 订单列表页订单列表页用于展示当前用户的预约记录便于用户跟踪状态。!-- 文件路径pages/order/order.wxml -- view classcontainer view classtabs view classtab-item {{currentStatus -1 ? active : }} bindtapchangeTab >// 文件路径src/main/java/com/example/petapp/controller/UserController.java RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public ResultString login(RequestBody LoginRequest loginRequest) { String openid userService.getOpenidByCode(loginRequest.getCode()); User user userService.findOrCreateUser(openid, loginRequest.getNickname(), loginRequest.getAvatar()); String token userService.generateToken(user); return Result.success(token); } }这里不粘贴完整业务代码因为不同项目的加密方式和 token 方案差异较大。在实际工程中token 可以使用 JWT也可以使用简单的 UUID 存 Redis视团队规范而定。5.3 创建预约订单接口创建预约订单时后端必须做三件事校验用户是否登录。校验参数合法性宠物归属、服务是否存在、日期是否合法。防止同一时段重复预约必要时对appointment_date time_slot staff_id加唯一约束或使用数据库锁。下面是核心代码思路// 文件路径src/main/java/com/example/petapp/service/AppointmentService.java Service public class AppointmentService { Autowired private AppointmentMapper appointmentMapper; Transactional(rollbackFor Exception.class) public void createAppointment(AppointmentCreateDTO dto, Long userId) { // 1. 校验宠物属于当前用户 Pet pet petMapper.selectById(dto.getPetId()); if (pet null || !pet.getUserId().equals(userId)) { throw new BusinessException(宠物信息不存在); } // 2. 校验服务存在且是上架状态 ServiceItem service serviceMapper.selectById(dto.getServiceId()); if (service null || service.getStatus() ! 1) { throw new BusinessException(服务项目不可预约); } // 3. 校验同一时段是否已被占用 int count appointmentMapper.countByStaffAndTime( dto.getStaffId(), dto.getAppointmentDate(), dto.getTimeSlot()); if (count 0) { throw new BusinessException(该时段已被预约); } // 4. 生成订单号并插入 Appointment appointment new Appointment(); appointment.setOrderNo(generateOrderNo()); appointment.setUserId(userId); appointment.setPetId(dto.getPetId()); appointment.setServiceId(dto.getServiceId()); appointment.setStaffId(dto.getStaffId()); appointment.setAppointmentDate(dto.getAppointmentDate()); appointment.setTimeSlot(dto.getTimeSlot()); appointment.setRemark(dto.getRemark()); appointment.setStatus(0); appointmentMapper.insert(appointment); } private String generateOrderNo() { return PET System.currentTimeMillis() RandomUtil.randomNumbers(4); } }5.4 取消预约接口取消预约在业务上要区分状态只有“待确认”状态下的订单允许用户直接取消已确认订单取消需要联系商家避免资源的无序释放。Transactional(rollbackFor Exception.class) public void cancelAppointment(Long appointmentId, Long userId) { Appointment appointment appointmentMapper.selectById(appointmentId); if (appointment null || !appointment.getUserId().equals(userId)) { throw new BusinessException(订单不存在); } if (appointment.getStatus() ! 0) { throw new BusinessException(当前状态不可取消); } appointment.setStatus(3); appointmentMapper.updateById(appointment); }6. 微信小程序常见问题与项目排错在开发宠物美容预约小程序时下面几个问题出现的频率很高提前整理出来能少走弯路。问题现象常见原因解决思路真机预览正常开发者工具白屏工具缓存、基础库版本不一致清缓存切换基础库版本重新编译页面顶部导航栏高度错乱不同机型状态栏高度不同使用wx.getWindowInfo()或胶囊按钮位置动态计算调用后端接口提示域名不合法未配置 request 合法域名在小程序后台配置 HTTPS 域名或开发工具勾选“不校验合法域名”wx.request请求一直 pending后台接口返回慢或未配置代理检查后端日志、网络环境、HTTPS 证书提交预约成功但订单列表不刷新页面 onShow 未重新请求数据在 onShow 生命周期中重新调用加载接口同一时段被多人预约成功缺少唯一约束或事务控制数据库加唯一索引后端用事务 行锁6.1 顶部导航栏高度适配很多开发者第一次处理自定义导航栏时都会踩到状态栏高度问题。推荐用官方 API 计算而不是写死一个数值。// 文件路径utils/layout.js function getNavBarInfo() { const windowInfo wx.getWindowInfo(); const menuButton wx.getMenuButtonBoundingClientRect(); return { statusBarHeight: windowInfo.statusBarHeight, navBarHeight: (menuButton.top - windowInfo.statusBarHeight) * 2 menuButton.height }; } module.exports { getNavBarInfo };6.2 分包异步化的使用场景当系统逐渐变大首页、预约、订单、宠物档案、商家后台等页面都放在主包中会导致主包体积接近 2MB 限制。此时可以使用分包加载。宠物美容预约系统在后期可以这样拆分{ pages: [ pages/index/index, pages/order/order, pages/mine/mine ], subPackages: [ { root: packageAppointment, pages: [ pages/appointment/appointment, pages/order-detail/order-detail ] }, { root: packagePet, pages: [ pages/pet/pet, pages/pet-edit/pet-edit ] } ] }分包可以显著减少主包体积但需要注意分包之间不能互相引用非公共代码。如果多个分包都要用到请求工具应该把utils放在主包中。7. 最佳实践与工程建议7.1 前端小程序开发建议统一请求封装所有接口都走同一个 request 工具方便在 header 中加入 token也方便统一处理 401 跳转登录。页面数据加载放在onShow中订单列表、个人中心这类数据经常变化的页面放在onShow可以在每次回到页面时刷新数据。合理使用setData不要一次性 setData 大量无关字段避免频繁更新导致页面卡顿。服务端校验优先小程序前端代码可以被反编译或修改所有关键业务规则必须以后端校验为准。7.2 后端开发建议订单号生成要考虑并发不能只用时间戳需要追加随机数或使用雪花算法。数据库事务创建预约订单时涉及宠物校验、服务校验、时段冲突校验、插入订单必须放在同一个事务中避免中间异常导致数据不一致。时间字段统一后端尽量返回yyyy-MM-dd HH:mm:ss格式的字符串避免前端处理时区问题。接口幂等性提交预约按钮可能被用户重复点击后端可以增加一个简单的幂等机制比如同一用户同一服务同一时段 5 秒内不允许重复提交。7.3 安全注意事项不要在小程序前端保存用户的 openid 和 session_key。后端接口需要校验身份不能只依赖前端传的 user_id。管理端接口必须做权限控制不能直接暴露在公网。日志中不要打印完整的用户手机号和微信 openid必要时应脱敏。7.4 上线前检查清单上线前可以对照下面这份清单做一轮自查小程序后台是否配置了 request 合法域名后端接口是否全部支持 HTTPS 访问是否删除了代码中的测试打印和调试弹窗宠物照片、美容师头像等图片是否使用 CDN 或对象存储是否设置了合理的会话超时时间并在前端做了 401 跳转处理是否配置了预约时段的上限防止同一时段无限接单8. 项目扩展方向宠物美容预约系统完成基础版本后可以从以下几个方向继续扩展支付能力接入微信支付用户线上下单时支付定金或全款。注意虚拟支付和实物支付的类目限制预约美容服务属于线下服务需要在小程序后台申请相应类目。优惠券和会员卡通过会员储值、次卡、优惠券提高用户复购。消息通知使用微信订阅消息提醒用户预约成功、预约开始前提醒。商家工作台提供一个独立的商家端小程序或者在小程序内增加员工角色视图让美容师能查看自己的当日排班。评价系统服务完成后引导用户评价为后续排名推荐提供数据支持。这几个方向也是真实宠物美容预约产品常见的商业闭环能够把单纯的预约工具变成一家门店的会员运营平台。9. 写在最后这篇文章从一个完整的宠物美容预约场景出发梳理了系统需求、数据库设计、小程序前端核心代码、后端接口设计、常见排错方法和工程化建议。如果你已经跟着本文完成了服务列表、预约下单、订单列表和宠物档案这几条核心链路那这套系统的骨架已经成型了后续无论是接支付、做商家端还是接入订阅消息都是在现有结构上的增量开发。实际开发中遇到的很多问题往往不是某个单独的技术点不会而是前后端协作时数据结构、接口语义、状态流转没有提前约定好。所以我的建议是动手写代码之前先把数据库表和接口文档定下来再用小程序端从首页到下单页完整走一遍流程这样开发效率会高很多。如果这篇文章对你有帮助可以先收藏备用。如果你在实现过程中遇到别的坑也可以在评论区分享你的排查过程大家一起交流。
返回列表