news 2026/10/15 7:08:01

SpringBoot+微信小程序实战:美甲店预约管理系统设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+微信小程序实战:美甲店预约管理系统设计与实现

做美甲店管理系统这件事,起因其实挺简单:有位开店的朋友跟我吐槽,预约全靠微信消息一条条对,会员卡余额记在本子上,月底一对账就头疼,遇上节假日更是手忙脚乱。我当时听完就说,这需求不用上太重的系统,一个 SpringBoot 后端加一个微信小程序就能搞定。这篇博客就把这个美甲服务管理小程序的完整项目拆开讲:为什么选 SpringBoot,功能模块怎么划分,数据库怎么设计,核心接口怎么实现,以及上线运行之后踩过的那些坑。项目源码完整可运行,编号 64788,如果你正打算给线下门店做一套预约管理系统,或者想找个全栈练手项目参考,可以直接拿来改。

1. 美甲服务管理小程序的目标:把店里的"人、单、钱"管顺

1.1 需求盘点:三个角色、四种核心场景

美甲店的业务流程和理发店类似,但又有自己的特点。美甲耗时通常很长,甲油胶、延长甲、手部护理动辄一两个小时,而且技师和项目的绑定关系很强,顾客指定的技师不一定所有项目都做。如果沿用传统的手写登记和微信口头预约,很容易出现档期冲突、到店等位、会员记录丢失这些老问题。

梳理完需求之后,我把整个系统拆成了三个角色、四种核心场景:

  • 顾客(小程序端):浏览项目、查看技师档期、预约座位、在线支付、储值开卡、消费后评价。
  • 店长(后台管理端):维护项目和价格、管理技师信息、查看每日预约排班、处理退款、看经营报表。
  • 技师(小程序/后台端):查看当天预约列表、确认或拒绝预约、服务完成后标记完工、查看个人业绩。

四种核心场景分别对应:首次到店前的预约决策、到店后的余额支付与划卡核销、离店后的评价与复购、以及店长的日常经营管理。这四种场景串起来,恰好覆盖了一家美甲门店从获客到留存的全过程。

1.2 为什么"小程序 + 后端"是这个场景的最优解

在项目启动之前,我也考虑过公众号 H5、独立 App 这些方案,但对比下来小程序优势太明显了。

其一,用户不用额外下载 App,微信里搜索一下或者扫门店码就能打开,使用门槛几乎为零。美甲顾客以年轻女性为主,她们对"用完即走"的小程序接受度很高,不想为了做一次美甲专门装个应用。

其二,微信生态自带支付能力,余额充值、订单支付都能在小程序里直接完成,不需要用户跳出到外部收银台。微信支付回调天然打通了"支付成功 -> 更新订单 -> 推送消息"这条链路。

其三,小程序的消息订阅能力可以代替部分电话通知,预约提醒、服务完成提醒、活动推送都能低成本触达用户。

而后端选择 SpringBoot,一方面是因为它足够成熟稳定,社区资料多,出了问题几乎都能查到解决方案;另一方面,一套 SpringBoot 服务既能支撑小程序接口,又能跑后台管理页面,还能给后续的公众号对接、数据分析留出余量,不会出现"换个业务方向就得重写后端"的情况。

2. 技术选型与项目结构:SpringBoot 够用又不重

2.1 后端技术栈:为了少写代码做的几个决定

这个项目后端用的是 SpringBoot 2.7 + MyBatis-Plus + MySQL 8.0 + Redis + Sa-Token。技术栈不算新,但每一样都是我对比过替代方案之后定下来的。

持久层选 MyBatis-Plus 而不是原生 MyBatis,核心原因只有一个:单表 CRUD 可以完全不写 SQL。美甲系统里像用户表、项目表、评价表这类基础数据,增删改查占了接口的一大部分,用 MyBatis-Plus 自带的 BaseMapper 方法就能搞定,省下的时间都花在了业务逻辑上。遇到多表关联查询,直接写自定义 SQL 放在 XML 里,也不影响灵活性。

权限认证选 Sa-Token 而不是 Spring Security,是因为小程序端只需要简单的登录态识别,Sa-Token 的 StpUtil.login() 一行代码就能签发 Token,拦截器配置也比 Security 的过滤器链直观得多。如果你更习惯 JWT 方案,也可以把 Sa-Token 换成手写 JWT 工具类,核心逻辑差别不大。

缓存方面,Redis 主要用来存三样东西:微信登录会话、首页轮播图缓存、热门技师列表缓存。预约数据本身更新频繁,不适合长期缓存,这个后面会细说。

2.2 小程序端为什么用原生而不是 uni-app

小程序前端我选的是原生 WXML/WXSS,没用 uni-app、Taro 这类跨端框架。原因是这个项目只服务微信一个平台,没有同时出支付宝小程序、抖音小程序的需求。用原生开发虽然写不了"一套代码多端运行",但胜在调试直观、组件生态兼容性最好,微信每年更新的新特性(比如最新的 Skyline 渲染引擎、同层渲染)可以第一时间用上,不用等跨端框架去适配。

项目管理上,代码分两块,互不干扰。后端是标准的 Maven 多模块工程,按业务拆分成 system(用户/权限)、appointment(预约)、order(订单支付)、member(会员卡)、marketing(营销活动)几个模块;小程序端则按页面目录划分,pages 下面对应 index、service、appointment、order、mine 等页面目录。前后端通过 HTTP 接口交互,统一返回结构为 { code, message, data },前端做了响应拦截器,code 非 0 直接弹提示。

3. 数据库设计:预约、会员卡、订单这几张表怎么串起来

3.1 不是最复杂,但要照顾业务闭环

数据库是整套系统的地基,设计阶段我反复改了三轮。第一轮只设计了用户、项目、预约三张表,后来发现订单支付和会员卡没法落,才重构出了现在的十一张核心表。

表名作用关键字段
user顾客信息openid, nickname, avatar, phone, balance, points
service_item美甲服务项目name, category, price, duration, cover_img, status
technician技师信息name, avatar, intro, service_duration_ratio
appointment预约记录user_id, item_id, technician_id, appoint_date, appoint_time, status, remark
order_info订单order_no, user_id, appointment_id, total_amount, pay_amount, pay_type, status
payment_record支付流水order_no, transaction_id, amount, channel, status, notify_data
member_card会员卡user_id, card_no, balance, points, status, expired_at
recharge_record充值记录user_id, card_id, amount, give_amount, pay_method, created_at
points_record积分流水user_id, points, type, remark
review评价user_id, appointment_id, score, content, images, reply
shop_config门店配置营业时间、公告、客服电话、轮播图 JSON

预约表是整个业务的中枢,它同时关联了用户、项目、技师,还通过 order_info 反查支付状态。在预约表上我加了一个唯一索引 (technician_id, appoint_date, appoint_time),这就是后面解决并发撞单的关键。

3.2 状态流转:预约和订单各走各的状态机

预约状态和订单状态是要分开管理的,很多新手容易把它们混成一个字段。我当时的做法是预约状态管服务履约过程,订单状态管资金流程。

预约状态机走的是:待确认 -> 已确认 -> 服务中 -> 已完成,中间可以随时分支到"已取消"。技师端点确认后,状态从待确认变成已确认;到店开始服务后点"开始服务",变成服务中;服务做完点"完成服务",预约置为已完成,同时触发积分计算和评价提醒。

订单状态机相对简单:待支付 -> 已支付 -> 已完成 / 已退款。这里有个关键点:如果用户用会员卡余额支付,订单表里记录 pay_type = balance,不生成微信支付流水,只扣减会员卡余额;如果走微信支付,则 pay_type = wechat,同时写入 payment_record。退款操作只允许针对微信支付的订单,余额支付的订单直接做余额退回,避免两边都要冲账的麻烦。

这个状态设计保证了以后加营销活动时,不会因为"钱已经退了但服务还没取消"产生脏数据。实际运营中遇到最典型的情况是:用户预约了晚上 7 点的延长甲,但临时改期。我的做法是不直接修改原预约,而是将原预约取消、生成一笔新的预约,订单金额多退少补。这样做虽然多一条数据,但每一笔操作都有迹可循,对账的时候不会糊涂。

4. 核心接口实现:从微信登录到余额支付的完整链路

4.1 微信登录:三行代码搞定身份识别

小程序的登录流程是固定的:前端调用 wx.login 拿到临时 code,后端拿着 code 去微信的接口换 openid 和 session_key,然后基于 openid 生成自己的登录态 Token 返回给前端。

@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { // 1. code 换 openid String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + dto.getCode() + "&grant_type=authorization_code"; JSONObject session = HttpUtil.get(url).getJSONObject(); String openid = session.getString("openid"); // 2. 查库,无记录则自动注册 User user = userService.getOne(new LambdaQueryWrapper<User>() .eq(User::getOpenid, openid)); if (user == null) { user = User.builder().openid(openid) .nickname("微信用户" + openid.substring(openid.length() - 6)) .build(); userService.save(user); } // 3. 签发 Sa-Token 登录态 StpUtil.login(user.getId()); return Result.ok(StpUtil.getTokenValue()); }

这里有个容易忽略的细节:用户昵称和头像不要直接拿微信资料里的默认值,因为很多用户从头像和昵称里能够识别出个人信息,涉及隐私合规。更稳妥的做法是先用 openid 做登录,等用户主动授权头像和昵称后再更新资料。小程序端的 wx.getUserProfile 接口现在也调整了授权策略,用户可能拒绝授权,所以程序里要兜底处理。

4.2 创建预约:分布式锁解决撞单

创建预约是并发冲突最集中的接口。两个用户可能同时选同一个技师的同一个时段,如果在数据库层不做限制,后写入的记录会把前面的覆盖掉,导致技师同时被两个顾客占用。

我的解决方案分两层。第一层是数据库的唯一索引,上面已经提到,(technician_id, appoint_date, appoint_time) 加了唯一约束,这是兜底防线,确保同一技师同一时段只能有一条未取消的预约记录。第二层是 Redis 分布式锁,在进入业务逻辑前先加锁,防止两个请求几乎同时通过查重判断。

@PostMapping("/create") public Result createAppointment(@RequestBody AppointmentDTO dto) { String lockKey = String.format("lock:appointment:%d:%s:%s", dto.getTechnicianId(), dto.getAppointDate(), dto.getAppointTime()); boolean locked = redisLock.tryLock(lockKey, 5, TimeUnit.SECONDS); if (!locked) { return Result.fail("当前时段预约人数较多,请换个时间试试"); } try { // 再次检查该时段是否已有预约 long count = appointmentService.count(new LambdaQueryWrapper<Appointment>() .eq(Appointment::getTechnicianId, dto.getTechnicianId()) .eq(Appointment::getAppointDate, dto.getAppointDate()) .eq(Appointment::getAppointTime, dto.getAppointTime()) .ne(Appointment::getStatus, "已取消")); if (count > 0) { return Result.fail("该时段已被约满,请选择其他时间"); } // 创建预约并生成订单,开启事务 return appointmentService.createWithOrder(dto); } finally { redisLock.unlock(lockKey); } }

tryLock 的等待时间我设的是 5 秒,实际测试下来足够了,因为正常创建预约的耗时在 200 毫秒以内。如果在高峰期,两个请求同时进来,后一个会在锁上等待,而不是直接打到数据库,这样数据库的并发压力也降下来了。

4.3 微信支付回调:幂等处理帮你省掉一半的坑

微信支付走的是统一下单 + 异步回调的模式。用户在支付成功后,微信服务器会往你配置的回调地址发一个通知,里面带订单号和交易流水号。回调处理最容易出错的地方是重复通知,微信会重试多次,如果你每次都去更新订单状态,就可能出现把已完成的订单重复退款的情况。

我的回调处理核心是:判断订单状态,只认第一次。

@PostMapping("/notify") public String notify(@RequestBody String xmlBody) { // 1. 验签,确认是微信官方通知 WxPayService wxPayService = new WxPayServiceImpl(); WxPayNotifyResult result = wxPayService.parseOrderNotifyResult(xmlBody); // 2. 根据商户订单号查本地订单 OrderInfo order = orderInfoService.getByOrderNo(result.getOutTradeNo()); if (order == null) { return "fail"; } // 3. 幂等判断:订单已经是已支付,直接返回成功 if ("已支付".equals(order.getStatus())) { return "success"; } // 4. 只有在待支付状态下才更新 order.setStatus("已支付"); order.setTransactionId(result.getTransactionId()); order.setPaidAt(LocalDateTime.now()); orderInfoService.updateById(order); return "success"; }

这里踩过一个很实在的坑:回调里更新订单状态的操作直接用了 updateById,如果订单已经支付且关联了预约,必须同步把预约状态从"待确认"改成"已确认"。这两个表不在同一个事务里,一旦更新完订单后程序崩溃,预约就卡在待确认状态,技师端看不到新单。我的做法是在第 4 步用一个事务方法同时更新两张表,并且回调接口的最外层包一层 try-catch,任何异常都不允许向外抛出未处理的错误,否则微信会一直重试,日志会被刷爆。

4.4 技师端接单与核销:确认、到店、完成三段式

技师端操作设计成三个动作:确认接单、开始服务、完成服务。这三个动作对应预约状态的流转,每个动作都通过一个独立接口完成,前端按钮的显隐根据当前状态控制。

确认接单时,后端会做一个小优化:同时给顾客推送一条预约确认消息。用的是微信的订阅消息功能,用户在小程序里主动订阅"预约状态通知"后才允许发送,不能无限次下发,这个限制需要在产品层面做好引导。完成服务时,后端自动计算积分(按消费金额 1:1 累计)写入用户表,同时生成一条积分流水,方便用户查看积分来源明细。

5. 小程序页面串联与用户体验:让顾客不教也会用

5.1 页面结构:按用户动线组织 Tab

小程序一共五个 Tab 页:首页、项目、预约、订单、我的。首页放门店轮播、公告、热门项目推荐,还有一键拨打电话;项目页是服务列表,按分类筛选(甲油胶、延长甲、手部护理、足部护理),点进详情能看到价格、耗时、效果图;预约页是核心表单页,依次选择项目、技师、日期、时段,底部实时显示金额;订单页分当前订单和历史订单两个 Tab;我的页面展示会员卡余额、积分、优惠券、收藏项目。

这个页面结构是按用户动线设计的:先逛,再选,然后约,最后看结果。没有为了展示功能而堆一堆复杂的入口,门店运营下来反馈也不错。

5.2 前端请求封装:两个拦截器省下大量重复代码

小程序原生的 wx.request 写起来比较啰嗦,我封装了一个 request.js 工具模块,统一处理 baseURL、请求头、Token 注入、错误提示和登录态失效跳转。

function request(url, method, data) { return new Promise((resolve, reject) => { wx.request({ url: config.baseUrl + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': getToken() }, success(res) { if (res.data.code === 401) { wx.navigateTo({ url: '/pages/login/login' }); reject(); } else if (res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message, icon: 'none' }); reject(); } }, fail(err) { wx.showToast({ title: '网络异常,请稍后重试', icon: 'none' }); reject(err); } }); }); }

wx.request 的成功回调里其实还包括 HTTP 状态码的判断,这里简化处理了。实际项目里我加了 500 错误码的全局提示,以及请求超时时间的配置。这些基础能力一次性封装好,后续每个页面只写业务逻辑,代码量至少少一半。

5.3 预约体验的三个细节

预约页是转化率最高的页面,体验上我专门打磨了三个细节。

第一,日期选择器用自定义的横滑组件,而不是小程序原生的 picker。因为 picker 的滚轮样式在视觉上太重,横滑组件每周 7 天一行,左右滑动切换,点到日期直接刷新下方时段列表,更贴近现在购票类应用的交互习惯。

第二,时段选择按门店营业时间生成,上午 10:00 到晚上 20:00,隔 30 分钟一个 slot,已经约满的时段直接置灰不可点。这些时段数据来自后端接口,返回的是当天技师排班和已约时段,前端做差集展示。

第三,创建预约成功后,页面不直接跳走,而是弹一个"预约成功"的浮层,显示预约号和门店信息,点击"知道了"回到订单页。这样做的好处是给用户一个心理确认的缓冲,也方便门店工作人员口头提醒加微信社群。

6. 上线之后的踩坑复盘与后续扩展方向

6.1 三个真实踩过的坑,希望对你有帮助

第一个坑是微信支付证书文件的路径问题。项目部署在云服务器上,商户平台下载的 apiclient_cert.p12 一开始放在 resources 目录下,本地测试没问题,打包上线后就报"读取证书失败"。原因是 SpringBoot 的 fat jar 会把 resources 里的文件打到 BOOT-INF/classes 里,Java 里用 File 直接读路径找不到。正确做法是把证书放到服务器上的独立目录,配置里写绝对路径,或者用 ClassPathResource 以流的方式读取。

第二个坑是数据日期格式的时区问题。小程序端传过来的日期是 "2025-01-15 19:30" 这种格式,后端如果用默认 Jackson 配置反序列化到 LocalDateTime,会遇到无法解析的情况。后来我统一在 application.yml 里配了 spring.jackson.date-format,并且约定前后端所有时间字段都用字符串传输,数据库存 DATETIME,Java 层用 LocalDateTime,转换逻辑集中在工具类里,避免每个接口各写各的解析逻辑。

第三个坑是预约取消后的时段释放。最初实现取消预约只是把状态改成"已取消",后来发现技师端还是能看到这个时间被占用,因为唯一索引把记录锁死了。我的解决方法是数据库层面再加一个"有效预约"的条件判断,查询可用时段时排除已取消和已完成状态。这块逻辑要改得彻底,前端置灰、后端查重、锁判断三处都要同步加过滤条件。

6.2 源码复用与二次开发方向

这套系统的源码我打包整理过,项目结构清晰,命名规范,注释也补全了一轮。如果你要拿去二次开发,有几个方向可以参考:

  • 增加营销模块:拼团、砍价、节假日满减券,这些场景在现有订单和支付链路之上扩展即可,不需要动表结构。
  • 对接门店库存:美甲耗材(甲油胶品牌、钻饰款)可以增加 sku 库存表,在技师完成服务时自动扣减。
  • 数据看板:后台目前只有简单的订单统计,可以接入一个图表组件,把每日营收、客单价、复购率做可视化。
  • 多门店支持:当前版本是单门店模型,如果要复制到连锁店,需要给预约表和技术员表增加 shop_id 字段,所有查询带上门店维度。

我在实际运行这个项目的过程中体会最深的一点是:做这类门店管理系统的难点从来不在技术本身,而在于把业务规则梳理清楚,让系统里的每个状态变化都对应现实中的一个动作。预约状态机、订单状态机、会员卡余额这三块一旦理顺,后面加任何新功能都会很顺。

最后分享一个操作层面的小技巧:小程序端和后端联调时不要让后端开发直接改生产环境数据库,给本地开发环境单独准备一套数据,甚至可以直接用 Docker 起一个 MySQL 容器,用脚本初始化表结构和测试数据。我前期就是因为贪方便直接连生产库调试,结果跑错了一次初始化脚本,把线上的会员卡数据洗掉了一部分,还好有备份才恢复过来。从那以后,我给自己定了个规矩:任何涉及数据结构的变更,先在测试库验证两次,再上生产环境。做项目,稳字当头。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 7:07:49

AI Agent 本质是可调度可验证的数字执行体,非智能聊天机器人

1. AI Agent 不是“更聪明的聊天机器人”&#xff0c;而是可调度、可编排、可验证的数字执行体很多人第一次听说 AI Agent&#xff0c;第一反应是&#xff1a;“不就是 ChatGPT 加个插件&#xff1f;”——这恰恰是当前最普遍也最危险的认知偏差。我带过三支从零搭建 Agent 系统…

作者头像 李华
网站建设 2026/10/15 7:07:30

蓝桥杯P12133产值调整:序列不降动态规划与Slope Trick优化

省赛结束那天晚上&#xff0c;两个算法交流群里同时刷屏讨论的&#xff0c;不是压轴题&#xff0c;而是这道产值调整。题号我记得很清楚&#xff0c;P12133&#xff0c;蓝桥杯2025省B组的一道经典序列调整类题目。这道题看起来“人畜无害”——无非就是改几个数让序列满足条件&…

作者头像 李华
网站建设 2026/10/15 7:07:28

Codex Composer predictions 深度解析:Pro 用户的代码预判实战指南

1. 这次更新到底改了什么1.1 从“补全”到“预判”的跨越先把这个标题拆开看。Codex 是 OpenAI 推出的代码生成模型系列&#xff0c;Composer 是集成在编辑器里的交互界面&#xff0c;predictions 则是它新开放的一项能力。把这三个词连起来&#xff0c;意思很直白&#xff1a;…

作者头像 李华
网站建设 2026/10/15 7:07:05

逆波兰表达式求值:双栈法实战与NOJ高频坑点解析

1. 项目概述&#xff1a;为什么逆波兰表达式是数据结构课绕不开的“试金石”在西北工业大学《数据结构》课程的NOJ在线评测系统里&#xff0c;“021-逆波兰表达式&#xff08;耿国华《数据结构——C语言描述》第7章第38题&#xff09;”这个编号几乎成了每届学生调试到凌晨的共…

作者头像 李华
网站建设 2026/10/15 7:06:57

Apache SkyWalking实战:全链路追踪的部署、存储与告警配置

提到分布式全链路追踪&#xff0c;我想先讲一个把我逼到墙角的问题。当时我们团队负责的电商系统刚完成微服务拆分&#xff0c;用户反馈"下单一直转圈&#xff0c;等十几秒才出结果"。我登录服务器查日志&#xff0c;从网关服务开始一个一个服务往下追&#xff0c;发…

作者头像 李华
网站建设 2026/10/15 7:06:40

白领通勤听书方案:把碎片时间变成自我提升的稳定输入系统

每天早高峰的地铁上&#xff0c;我经常看到一种很典型的画面&#xff1a;人挤人站着&#xff0c;一只手扶着扶手&#xff0c;另一只手举着手机&#xff0c;屏幕上滚着短视频。下车以后大家表情都很疲惫&#xff0c;但聊起来多半会感叹“一上午啥也没干”。而我自己的耳朵里&…

作者头像 李华