“宠物经济这几年有多火,不用我再多说了。但真去跟开宠物店的朋友聊一圈你就会发现:大部分门店的预约还在靠微信群接龙、前台手写登记,会员卡要么是纸质小本子,要么是老板脑子里的Excel。客户问一句‘我家狗下次洗澡是什么时候’,店员要翻半天聊天记录。这个局面,对于一个会做一点系统开发的从业者来说,机会感非常强——用SpringBoot搭配微信小程序,做一套宠物会所的会员服务预约管理系统,正好能把‘服务预约+会员经营’这两件最核心的事一次性理顺。”
这套系统解决的不只是“客户能在线约个时间”,它的价值在于把整个门店的服务流程串起来:用户在小程序上维护宠物档案、选服务项目、预约时间、用会员卡或余额结算;门店在管理端维护服务项目、美容师排班、预约审核、会员卡种与充值记录。前后端数据打通之后,门店老板能看到今天来了几个客户、哪些服务最赚钱、哪些时间段排队最久,这些数据才是传统小店最缺的东西。
整套系统非常适合三类人参考:一是正在帮宠物门店做数字化升级的开发者和产品经理,二是想给自己店里做一套内部管理工具的门店经营者,三是用这类项目练手、想完整走一遍“小程序+后端+支付+部署”全流程的后端学习者。全文用的是我实际操作中的完整思路,从功能拆解、表结构设计到接口联调、避坑记录,按一条能落地的路线展开。
1. 项目背景与整体功能设计
1.1 宠物会所的真实痛点是什么
先别急着写代码,把业务场景想清楚,后面能少改一半需求。宠物会所跟一般的美容店、理发店有个很大区别:服务的对象是宠物,决策的人是主人。这就带来两个问题,一是主人往往不清楚自己的宠物适合什么服务(洗澡频率、皮肤状态、是否应激),二是每一单服务都需要同时关联“人”和“宠物”两条信息线,门店要维护的不只是客户的联系方式,还有宠物的品种、年龄、体重、疫苗情况、过敏史。这些信息如果全部靠前台在电话里问,效率极低,而且容易出错。
传统预约方式的问题我帮朋友梳理过,总结下来就是这几点:
- 预约冲突靠人工判断:美容师一天能接8个宠物洗澡,前台接电话时不一定记得住每个人的排班,经常出现两个客户约同一个时间点,到店后扯皮。
- 会员资产不透明:客户办了卡,消费几次之后记不清余额,前台查询慢,客户体验差。
- 无宠物档案沉淀:宠物换主人、换门店之后,健康信息完全断裂,下一家店要重新问一遍。
- 爽约率高:没有预约提醒,客户忘了时间,美容师空等,门店损失的是实打实的工位成本。
系统要设计的就是把这几条线的信息全部数字化。用户端不需要教客户“怎么用”,打开小程序能看到当前门店的可用服务、查询宠物档案、一键预约,体验上和订电影票类似;管理端则是给前台和店长用,每天的工作台就是处理预约单、确认服务完成、扣减会员卡余额。
1.2 系统功能模块拆解
整个系统按使用角色分两条线,三条大模块:
- 用户端小程序:微信登录授权、宠物档案管理(添加/编辑/删除宠物)、服务项目浏览(图文、价格、时长)、在线预约(选服务、选宠物、选时间)、会员卡与余额查询、消费记录明细、预约记录跟踪(待确认/已确认/已完成/已取消)、微信支付(余额不足时充值)。
- 管理端:仪表盘(今日预约数、营收金额、活跃会员数)、服务项目管理(上架/下架/调价)、员工与排班管理(美容师、助理的排班时间段)、预约单管理(审核、确认、完成、取消)、会员与充值管理(发卡、充值、冻结)、消费记录与营收统计。
- 消息触达:预约提交后通知门店端,预约确认后通过小程序订阅消息通知用户;服务前一天自动发出提醒,降低爽约率。
这里特别值得注意的一点是“预约单”的设计。它不能只存一个服务项目ID,因为一个时段内一个美容师可能接待多个宠物,一张预约单要能记录:哪个用户、哪只宠物、预约了哪个项目、期望时间段、实际到店时间、指派给哪位美容师、服务完成后扣除的是哪个会员卡种的余额。这条数据链路设计清楚,后面做统计和结算才不会乱。
1.3 为什么用户端选微信小程序而不是App或H5
选微信小程序做C端入口,核心原因是获客成本和用户习惯。宠物主人在店里消费之后,店主最希望的就是“加个微信,下次直接在微信里约”,小程序天然在微信生态里,不用下载App,扫个码或者从聊天记录里点一下就能打开。加上微信支付的闭环体验,充值、买单全部在微信里完成,不用跳外部App,转化率高很多。
从开发端来说,小程序也有明显的成本优势。微信官方提供了一套完整的前端组件和API(登录、支付、订阅消息、地图定位),一个人前端后端全干完全可行。相比做一个单独的App要适配iOS和安卓两套体系,小程序这种“用完即走”的轻量形态,和宠物服务这种低频但刚需的消费场景正好匹配。
2. 技术选型分析与架构设计
2.1 后端框架为什么押注SpringBoot
后端我选了SpringBoot 2.7.x + MyBatis Plus + MySQL这套组合,这是目前单体业务系统里最顺手的搭配,没有之一。对于这种体量(单店或几家连锁、几十个服务项目、几千个会员)的系统,SpringBoot的生态成熟度和开发效率是最平衡的。
- 起步快:SpringBoot内置了Tomcat,配置简化,一个可运行的Jar包就能部署上云服务器。
- 生态完善:无论是集成Redis做缓存、集成微信支付SDK,还是集成定时任务做提醒推送,社区都有现成方案。
- 业务代码好维护:Controller、Service、Mapper三层划分清晰,后面接小程序端的开发,接口文档也好对。
有人会问:这种项目用Node或Python的Flask更快吧?我个人的体会是,如果这个项目后面还想扩展成连锁店的模式,SpringBoot的稳定性、多数据源支持、成熟的ORM体系要明显优于轻量脚本后端。而且国内云服务器上跑Java的运维经验最丰富,出问题好查资料。
2.2 小程序端的实现路线
小程序端有两条路线可选:纯原生WXML/WXSS/JS,和uni-app跨端方案。我这次用的是原生小程序,原因是业务功能本身不复杂,原生API用起来最直接,不用引入额外的编译层。特别是微信登录、微信支付、订阅消息这三个核心能力,官方文档和社区案例都是基于原生语法讲解的,跟着做不容易踩坑。
如果团队之前有Vue经验,选uni-app也没有问题,写一套代码还能顺便出H5版本。但要注意的是,uni-app在调用微信原生能力时偶尔要做条件编译,遇到版本更新还得等插件适配。纯原生虽然重复代码多一点,但胜在可控。
2.3 SpringBoot工程目录结构
实际的工程结构我按业务模块分包,而不是按技术分层分包,后面多人协作时找文件更方便:
com.petclub ├── config // 全局配置:拦截器、微信参数、线程池 ├── controller // 接口层 │ ├── user // 小程序端接口 │ └── admin // 管理端接口 ├── service // 业务逻辑层 │ ├── appoint // 预约相关 │ ├── pet // 宠物档案相关 │ ├── member // 会员卡相关 │ └── pay // 支付相关 ├── mapper // MyBatis Plus Mapper接口 ├── entity // 数据库实体 ├── dto // 接收参数对象 ├── vo // 返回视图对象 ├── common // 统一返回体、异常处理 ├── task // 定时任务(预约提醒、过期处理) └── utils // 工具类(JWT、日期处理、微信API)Controller层只做参数校验和调用Service,具体的业务判断全部下沉到Service中。这个小细节很多人不注意,但等到管理端和用户端都要调用同一个“取消预约”逻辑时,就会体会到Service复用的价值了。
2.4 部署方案与服务器配置
部署上云用的是最省钱的组合:1核2G的云服务器 + 阿里云或腾讯云的云数据库MySQL,再加一台Redis(或者同一台服务器上起一个Redis实例)。这个配置跑这套系统完全没有压力,毕竟单店场景撑死同时在线几十人。
域名方面要提前准备,微信小程序要求所有后台接口必须是HTTPS,而且域名要备案、要配置合法域名校验。这块是新手最容易卡住的地方,我后面专门写一节排错记录。
提示:小程序发正式版要求服务器域名必须在小程序后台配置白名单,并且需要ICP备案,开发阶段可以用“不校验合法域名”来临时调试,但正式上线前一定要把域名和HTTPS证书配好。
3. 数据库设计与核心业务逻辑
3.1 核心数据表结构与设计意图
这套系统的数据表做完基本就在10张以内,核心表我是按“履约链路”来设计的:用户(谁买)→ 宠物(给谁服务)→ 服务项目(买什么)→ 预约单(什么时候履约)→ 会员卡与消费记录(怎么结算)。
| 表名 | 用途 | 关键字段 |
|---|---|---|
| member_user | 会员用户 | openid、昵称、手机号、余额 |
| pet_profile | 宠物档案 | 所属用户、宠物名、品种、体重、疫苗情况 |
| service_item | 服务项目 | 项目名、价格、时长、类型(美容/寄养/诊疗)、封面图 |
| service_appointment | 预约单 | 用户ID、宠物ID、项目ID、员工ID、时段、状态 |
| employee_info | 员工 | 姓名、岗位(美容师/医生/助理)、服务状态 |
| employee_schedule | 排班 | 员工ID、日期、可用时段 |
| member_card | 会员卡 | 卡种、余额、充值赠送、有效期 |
| consume_record | 消费记录 | 关联订单、扣减金额、余额快照 |
这里重点说一下宠物档案表。它不只是“名字+品种”这种基础信息,还要存“洗护备注”,比如“对吹风机敏感,只能用静音模式”“右后腿有点跛,洗澡时注意防滑”。这种细节在预约单确认时被带到当天的工作任务里,客户会觉得这个店非常专业,实际上只是后台数据设计得细了一点点。
3.2 预约时间冲突检测的设计思路
预约系统最核心的业务逻辑就是“时间冲突检测”。打个比方,美容师小张一天8个工位,上午10点到11点这个时段有客户约了给金毛洗澡,那这个时段就不能再排别的项目。我在设计时用了一种最简单有效的方式:预约单里直接记录“开始时间 + 结束时间 + 员工ID”,冲突判断用SQL查重叠区间。
SELECT COUNT(*) FROM service_appointment WHERE employee_id = #{employeeId} AND status IN ('PENDING', 'CONFIRMED', 'COMPLETED') AND start_time < #{endTime} AND end_time > #{startTime}这条SQL的思路是:只要现有预约的开始时间早于新预约的结束时间,并且现有预约的结束时间晚于新预约的开始时间,就说明两个时间段有交集。COUNT大于0就提示“该时段已被预约,请选择其他时间段”。
这个判断逻辑写清楚之后有两个好处:一是可以顺便做“服务时长校验”,比如美容项目是90分钟,用户选了14:00到15:30,时长对了才允许提交;二是如果以后做连锁店,只需要在表里加一个门店ID,同样的SQL就能适配多门店隔离。
3.3 会员卡余额结算与充值设计
会员卡设计这块有个细节容易被忽略:充值和消费不能只改余额字段,一定要记录流水明细。否则客户投诉说“我充了500怎么余额不对”时,你没有证据链可以追溯。
我的处理方式是:
- 余额变更只允许通过消费记录表来驱动,每一笔扣减都对应一条consume_record,记录“变动前余额、变动金额、变动后余额”三个快照。
- 充值赠送规则单独建配置表。比如充300送30、充500送80,这部分赠送金额要标记为“赠送余额”,因为很多商家在客户退卡时要扣回赠送部分,不区分会导致财务账对不上。
- 会员卡到期时间用日期类型,消费前判断有效期;到期后自动转为“已过期”状态,待续费后重新激活。
举例说明:用户小红办了张500元的卡,系统赠送了80元,宠物洗澡扣了一次128元,账户余额变为452元(含赠送余额)。在consume_record里就有三条数据:一条充值流水,一条赠送流水,一条消费流水,余额字段全部有据可查。
3.4 微信支付与回调处理的注意点
微信支付接入时最大的坑不是调起支付,而是回调通知的处理。支付成功后,微信服务器会异步调用你配置的支付回调地址,通知你“这笔订单支付成功了”。如果回调处理不稳定,用户钱付了但余额没到账,投诉率会直线上升。
我在回调逻辑里做了两件事保证幂等:一是用商户订单号作为唯一索引,处理之前先查询是否已处理过;二是整个回调处理逻辑放在数据库事务里,先更新订单状态再增加余额,两个操作要么都成功,要么都失败。
@Transactional public void handlePayNotify(PayNotifyVO notifyVO) { String orderNo = notifyVO.getOutTradeNo(); RechargeOrder order = rechargeOrderMapper.selectByOrderNo(orderNo); if (order != null && order.getStatus() == PayStatus.PAID) { // 已经处理过,直接返回,避免重复入账 return; } // 更新订单状态 // 增加会员余额 // 写入消费记录 }4. 小程序端实现与后端接口联调
4.1 微信登录流程的前后端配合
微信小程序的登录机制跟传统Web登录完全不同。用户点击授权之后,小程序端拿到的不是用户的手机号或昵称,而是一个临时凭证code,这个code需要通过后端接口去微信服务器换取openid,openid才是用户的唯一身份标识。
整个流程是这样的:
- 小程序端调用 wx.login(),获取临时code。
- 小程序端把code发送到后端接口 /api/user/login。
- 后端拿着code + AppId + AppSecret 调用微信的 jscode2session 接口。
- 微信返回 openid 和 session_key,后端用 openid 查用户表,新用户自动注册,老用户直接登录。
- 后端生成一个自定义登录态token(我用JWT)返回给小程序,后续所有请求都带这个token。
这里有个经验之谈:不要在前端做太多用户信息缓存,用户授权获取的昵称头像存到后端即可,后续每次登录都用wx.login重新换取code,不要在本地存openid。原因很简单,小程序端缓存的数据可以被篡改或清空,而后端每次都能从微信服务器拿到最准确的openid,这才是信任的来源。
// 小程序端 wx.login({ success: async (res) => { const loginRes = await request({ url: '/api/user/login', data: { code: res.code } }); wx.setStorageSync('token', loginRes.data.token); } });4.2 小程序首页与预约流程的页面拆解
小程序端的页面按用户操作路径来组织,尽量让用户3步之内完成预约:
- 首页:门店介绍、服务项目推荐、近期优惠活动、快捷预约入口。
- 服务列表页:按类型分类展示服务项目,点击详情查看项目图、价格、耗时和注意事项。
- 预约页:核心流程页,从上到下分别是“选择宠物(从我的宠物档案里选)”“选择服务项目”“选择日期”“选择可用时间段”“确认提交”。
- 我的页面:宠物档案管理、会员卡余额查询、预约记录、充值入口、个人资料设置。
预约页的“选择可用时间段”是前后端配合的重点。前端先选好日期,然后请求后端接口获取当天所有美容师的排班情况和已占用时段,后端会返回“当前日期剩余可预约时间段”,而不是把原始排班数据全部暴露给前端。否则前端还得自己算冲突,既费流量又容易算错。
4.3 后端统一返回体与异常处理的约定
前后端联调最容易出问题的地方就是接口返回格式不统一。我在项目开始前就定了一个统一返回体,所有接口无论成功失败都返回同样的结构:
{ "code": 200, "message": "success", "data": {} }- code为200表示成功,非200表示业务失败,比如4001表示“参数校验失败”,4002表示“未登录或登录已过期”,5000表示“该时段已被预约”。
- 后端用全局异常处理器统一拦截所有未捕获异常,防止出现500错误时把内部堆栈信息直接返回给小程序端(这既不安全也不友好)。
- 小程序端封装一个request工具,统一处理code码:登录过期时自动跳回登录页并清空缓存,其他错误用Toast提示用户。
这个约定看起来简单,但能避免联调阶段大量无意义的沟通成本。某同学第一次做前后端联调时,后端接口失败时返回的是一大段英文报错,前端根本不知道怎么处理,后来统一了格式之后双方效率都高了很多。
4.4 接口鉴权与用户数据隔离
所有涉及用户信息的接口都必须校验登录态。我用SpringBoot拦截器实现了一个简单的JWT鉴权:小程序端请求时在Header里带Authorization字段,拦截器解析token,从里面拿出用户ID放到请求上下文中,Service层直接获取当前用户。
这里有个安全细节:查询预约记录、修改宠物档案这类操作,SQL里一定要带上“所属用户ID”条件,而不是只靠token里的用户ID去查。说白了就是后端不能信任任何前端传上来的ID参数,必须从token里取。
// 错误示例:直接按前端传入petId查询 Pet pet = petMapper.selectById(req.getPetId()); // 正确示例:校验宠物确实属于当前用户 Pet pet = petMapper.selectByIdAndUserId(req.getPetId(), currentUserId); if (pet == null) { throw new BusinessException("宠物不存在或无权操作"); }5. 预约管理与消息通知功能解析
5.1 预约单的一整套状态流转
预约单不是订完就结束了,它要经历从“提交”到“完成”甚至“取消”的多个状态。我把状态设计成五个:待确认、已确认、已完成、已取消、已爽约。
- 提交预约后,状态是“待确认”。门店前台看到新订单,根据排班情况决定是否确认(一般系统已经做过冲突检测,确认只是走个流程)。
- 前台确认后状态变成“已确认”,这时候要触发消息通知给客户,告诉TA预约成功了。
- 服务做完,前台在管理端点“完成服务”,此时系统才会真正扣减会员卡余额、生成消费记录。
- 客户在服务开始前两小时可以自行取消,状态变为“已取消”;过了这个时间未取消又未到店,门店可以手动标记“爽约”。
这五个状态的流转全程记录操作人和操作时间,后续如果要复盘“这个月为什么爽约率高”,看数据就能分析出是否是周四下午这种容易忘记的时间段。
5.2 小程序订阅消息触发时机与用户授权
订阅消息是微信提高小程序触达率的重要能力,但它的权限是一次性的,也就是说用户只要授权一次,你只能给TA发一条消息。这就要求开发者必须在最合适的时机请求授权,并且把每一类消息用在刀刃上。
我这次的用法是:
- 首次进入小程序时,弹出授权框请求“预约状态提醒”和“服务进度提醒”两个模板的订阅授权。
- 如果用户点了“总是保持以上选择”,后端在预约状态变化的几个节点(确认成功、服务完成、取消成功)都能给用户推送消息提醒。
- 服务开始前一天的19:00,定时任务扫描第二天的预约单,批量给用户发送“您明天的预约即将开始”的提醒,降低爽约率。
消息内容不要写得像系统通知一样冷冰冰,可以用“宝贝洗澡提醒”这种口语化文案,点击之后直接跳转到小程序的预约详情页,对用户来说体验更好。
// 定时任务示例:每天19点发送明日提醒 @Scheduled(cron = "0 0 19 * * ?") public void sendTomorrowRemind() { List<Appointment> tomorrowList = appointmentService.listTomorrowAppointments(); for (Appointment appointment : tomorrowList) { subscribeMessageService.sendAppointmentRemind(appointment); } }5.3 管理端仪表盘与经营数据呈现逻辑
管理端是给店长看的,设计上更重数据而不是重交互。仪表盘页面展示四个核心卡片:今日预约数、今日待服务数、本月营业额、本月新增会员数。下方是本周每天预约量的柱状图(ECharts),旁边是服务项目销售排行Top5。
这些数据的统计口径要注意:营业额统计的是“已完成服务”的金额,而不是“已预约”的预估金额,否则容易高估。新增会员数统计的是“当日注册并首次充值”的用户,那些只登录没消费的不算。口径清楚了,数据才有参考价值。
我实际参与的门店项目中,店长最看重的一个数据是“服务项目销售排行”。如果连续一个月“精致洗护”卖得比“基础洗护”多,说明客户群体消费能力强,可以适当增加高端项目的排班资源,这个结论靠直觉可不容易得出来。
6. 常见问题与排查技巧实录
6.1 微信登录状态失效导致接口报401
联调阶段最容易碰到的问题就是token过期或无效,前端拿不到数据,后端日志报“未登录”。排查思路其实不复杂:
- 先看后端拦截器日志,确认请求是否真的到达了后端。如果没到,多半是前端请求头里的token没带上。
- 到了后端但报401,用在线工具解析一下token,看payload里的过期时间是否小于当前时间。
- 还有一种是服务器时间和本地时间不同步。JWT的签发和校验都依赖时间戳,如果服务器时间比实际时间晚几个小时,刚签发的token启动就变成“过期”了。解决方法是运维层面统一做NTP时间同步,业务代码里签发token时过期时间按实际业务时长往后推,不要只给几小时。
6.2 预约时段查得到但提交时提示已被占用
这个问题非常经典,属于“前端展示的可用时段”和“后端提交时校验的时段”不一致造成的。原因是页面打开之后,用户停留了几分钟再提交,这段时间里另一个客户可能把那个时段约走了。
解决办法是后端提交预约时重新做一次冲突检测,而不是只信任前端传过来的结果。提示语要友好:“该时段刚刚被预约,请重新选择时间”,同时前端自动刷新可用时段列表。这个设计虽然会让个别用户觉得“稍等”,但避免了数据错乱引发的更严重投诉。
6.3 小程序线上报错“url not in domain list”
很多开发者在本地拿真机调试时一切正常,一上传体验版就报这个错。原因是微信小程序要求所有网络请求的域名都必须在“小程序管理后台-开发管理-服务器域名”里配置白名单,而且必须是HTTPS协议。
排查步骤:
- 确认后端域名已完成ICP备案,且已申请并部署SSL证书。
- 在微信公众平台小程序后台,把接口域名分别填入request合法域名、uploadFile合法域名(如果有图片上传)。
- 配置完成后等几分钟再试,微信的域名校验是实时的,但偶尔有缓存。
这个坑我踩过,某次给客户部署时域名备案一直没下来,临时用另一台服务器IP调试,结果总是报错。后来才知道,开发阶段可以在开发者工具里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”,但这条只在开发版和体验版里生效,正式版必须真实配置。
6.4 支付回调延迟与重复通知的处理
微信支付的回调通知虽然通过异步方式推送,但在极端情况下可能出现重复通知或通知延迟。比如用户充值后,前端一直显示“支付处理中”,过了10秒才收到回调结果。
处理这个问题的原则就一句话:回调逻辑必须幂等,前端不能依赖回调的实时性。我的做法是:
- 前端在发起充值时,生成一个商户订单号,并记录“支付中”状态。
- 支付成功回调之后改状态为“已支付”,同时更新余额。
- 前端定期查询订单状态接口,即使回调延迟也不影响最终结果展示。
- 订单表里对商户订单号做唯一索引,数据库层面保证重复回调不会重复入账。
支付这块一定要在测试环境多模拟几遍:发起充值、中途关掉小程序、支付成功后杀掉进程再做一次回调,几种异常场景全部跑通之后再上线。
7. 项目扩展与个人经验总结
这个项目做到可上线运行的程度之后,其实还有不少可以继续扩展的方向。我在实际接手宠物门店的项目时,后续一般会遇到这样几个需求迭代点:
- 多门店支持:预约表增加门店标识,管理端按门店维度隔离数据,服务项目可以统一维护,但各门店排班独立。
- 寄养业务日历化:寄养服务的周期是“天”而不是“小时”,需要引入一个日历视图展示笼位占用情况,这块逻辑跟洗护预约完全不一样。
- 营销活动能力:比如“老带新送券”“充值满减”“节假日限时折扣”,本质上是在会员卡和消费记录之间再增加一层优惠券表。
- 数据报表升级:从单纯统计营业额变成按服务类型、员工维度、客户贡献值做交叉分析,给门店经营决策提供数据支撑。
如果读者是一个刚接触这类项目的人,我最想说的一个经验是:先把业务表设计清楚,再去写代码。这个项目里的预约冲突检测、会员余额结算、支付回调幂等,任何一个模块出问题都需要改表结构才能根治,而改表结构在项目中期是成本最高的事。花两天时间把ER图画透,后面能省两周的调试时间。
关于技术选型,还有一个建议:如果只是帮一家小店做内部工具,不需要一上来就上微服务、上消息队列。SpringBoot单体能解决95%的生意场景,复杂度是后期业务量大了再加的,不要在第一天就给自己挖坑。
最后分享一个小细节:我在给小程序接口写返回数据时,所有时间字段都返回时间戳(long型)而不是格式化字符串。小程序端拿到时间戳之后自己格式化,可以规避时区问题和iOS与安卓的日期格式兼容问题。这个习惯帮我少处理了很多「这边显示正常那边就变NaN」的诡异Bug。整套系统从需求梳理到上线跑了大概三周,我一个人干完了后端、小程序和部署,如果你也有一个相对完整的业务场景想落地,这套方案的参考价值都非常直接。