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

资讯详情

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

宠物美容预约系统设计:状态机、冲突检测与微信登录实战

宠物美容预约系统设计:状态机、冲突检测与微信登录实战 宠物美容预约系统是课程设计和面试项目中出镜率很高的选题表面看功能很简单用户选时间、选服务、提交预约商家确认一下就行。但真正上手实现时你会发现它远不是“几个增删改查接口”那么简单微信登录态怎么换、预约状态怎么流转、同一时间段被两个人同时约了怎么办、商家端怎么知道今天有哪些单子没做——每一个都是具体问题。这篇文章会先拆解宠物美容预约系统的完整设计再重点讲预约系统中比 CRUD 更核心的三件事状态机、时间冲突检测、微信登录态设计。整体以后端 Spring Boot 小程序原生框架为例展开但思路同样适用于 Node.js、Python 后端或者小程序云开发。读完之后你可以直接把这套模式迁移到美发预约、家政预约、陪诊预约等几乎所有的预约类项目上。1. 预约系统真正要解决的问题很多初学者拿到“宠物美容预约系统”这类题目后第一反应是设计用户表、宠物表、预约表然后写上几个接口用户提交预约、管理员查看列表、点击确认。这套流程看起来完整但距离“能上线用的预约系统”还差得很远。预约类系统真正要解决的不是 CRUD而是业务状态的可控流转。一次宠物美容预约从用户提交那一刻开始状态至少经历以下变化待确认用户已提交商家还没处理。已确认商家接单认定这个时段可以做。服务中宠物已经开始美容。已完成服务结束流程关闭。已取消用户或商家主动取消。已爽约到约定时间用户没来且没有提前取消。如果代码里到处散落着setStatus(3)这种写法一开始还能运行但业务规则一多就会失控。比如“已完成的服务不允许再取消”“爽约的预约不允许自动变成已完成”“取消后原时间段要释放出来给其他用户预约”。这些规则不是 CRUD 能保证的必须让状态流转走统一入口。第二个容易忽略的问题是时间冲突。宠物美容通常按项目计费但真正占用的是美容师的时间段。一个美容师同一时间只能服务一只宠物所以系统必须保证同一个门店、同一个时间段不能出现两条重叠的有效预约。这里“有效”指的是待确认、已确认、服务中三种状态。如果只判断“预约表里有没有记录”已经取消的预约会把时间段永久占住体验会很差。第三个问题是微信登录态。小程序端不能保存 AppSecretwx.login拿到的 code 只能交给后端由后端调用微信接口换取 openid 和 session_key。很多新手把这一步做成“前端直接传 openid”这是完全错误的安全设计。后面会详细展开。所以这篇文章的核心判断是预约类项目的难点不在接口多而在状态机设计、时间冲突检测、登录鉴权链路这三条主线上。把这三点想清楚这个项目的架构就是成立的。2. 系统功能拆解与整体架构2.1 功能模块划分宠物美容预约系统按使用角色可分为用户端和商家管理端。用户端的主要功能浏览宠物美容服务项目查看价格和预计耗时。添加宠物档案包括宠物昵称、品种、体重、备注。选择门店、服务项目、日期和时段提交预约。查看自己的预约列表按状态筛选。取消处于“待确认”或“已确认”状态的预约。接收预约确认或取消的通知。商家管理端的主要功能查看全部预约按日期、状态、门店筛选。确认或取消预约。将预约标记为“服务中”“已完成”。管理服务项目、门店和美容师基本信息。整体不是一个高并发系统核心诉求是业务状态清晰、操作可追溯、时间不冲突。2.2 技术选型对比方案优点缺点适合场景小程序原生框架 自建后端学习路径清晰后端可控性强贴近实际企业项目需要自己维护服务器、数据库、部署链路课程设计、毕业设计、想系统学习全栈开发小程序原生框架 微信云开发免运维云函数直接读写云数据库登录可直接用云开发身份认证云开发有一定平台绑定业务复杂后不如自建后端灵活快速上线 Demo、个人小工具uni-app 跨端开发一套代码可编译到微信小程序、H5、App调试多了一层编译出问题需要区分框架层和业务层需要多端发布的产品本文以“原生小程序 Spring Boot MySQL”为主讲方案并不是说这个组合一定最好而是它最能说明白预约系统的完整链路。如果你用的是云开发状态机、时间冲突、数据表设计的核心思路完全一致区别只是接口层换成了云函数。2.3 架构分层后端按常见的分层结构组织控制层Controller接收前端请求做参数校验返回统一响应结构。业务层Service承载核心业务逻辑包括登录态校验、预约创建、状态流转、冲突检测。数据访问层Mapper/DAO操作 MySQL 数据表。前端小程序分为页面层和工具层。页面层负责展示和交互工具层封装request请求、登录状态处理、时间格式化等公共逻辑。这种分层的好处是后续新增一个“取消预约”功能只需要加一个接口、加一个页面方法不需要把现有代码大改一遍。3. 数据库设计与核心表结构3.1 核心表设计预约系统至少需要这几张表用户表、宠物表、服务项目表、预约记录表。这里给出一个可落地的 MySQL 表结构实际项目可以按业务扩展门店表和美容师表。用户表CREATE TABLE pet_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 用户ID, openid VARCHAR(64) NOT NULL COMMENT 微信openid, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, avatar_url VARCHAR(500) DEFAULT NULL COMMENT 头像地址, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;宠物表CREATE TABLE pet_profile ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 宠物ID, user_id BIGINT NOT NULL COMMENT 所属用户ID, name VARCHAR(30) NOT NULL COMMENT 宠物昵称, breed VARCHAR(30) DEFAULT NULL COMMENT 品种, weight DECIMAL(5,2) DEFAULT NULL COMMENT 体重kg, remark VARCHAR(500) DEFAULT NULL COMMENT 注意事项, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物档案表;服务项目表CREATE TABLE service_item ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 项目ID, name VARCHAR(50) NOT NULL COMMENT 项目名称, price DECIMAL(10,2) NOT NULL COMMENT 价格, duration_minutes INT NOT NULL COMMENT 预计耗时(分钟), description VARCHAR(500) DEFAULT NULL COMMENT 项目说明, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT美容服务项目表;预约记录表CREATE TABLE appointment ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 预约ID, appointment_no VARCHAR(32) NOT NULL COMMENT 预约单号, user_id BIGINT NOT NULL COMMENT 用户ID, pet_id BIGINT NOT NULL COMMENT 宠物ID, service_item_id BIGINT NOT NULL COMMENT 服务项目ID, shop_id BIGINT DEFAULT NULL COMMENT 门店ID, appointment_date DATE NOT NULL COMMENT 预约日期, start_time DATETIME NOT NULL COMMENT 预计开始时间, end_time DATETIME NOT NULL COMMENT 预计结束时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态 0待确认 1已确认 2服务中 3已完成 4已取消 5已爽约, remark VARCHAR(500) DEFAULT NULL COMMENT 用户备注, cancel_reason VARCHAR(200) DEFAULT NULL COMMENT 取消原因, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_appointment_no (appointment_no), KEY idx_user_id (user_id), KEY idx_status (status), KEY idx_start_time (start_time), KEY idx_shop_time (shop_id, start_time, end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物美容预约记录表;3.2 预约记录表的设计要点start_time和end_time使用 DATETIME 而不是分开存储日期和字符串时间主要原因是时间比较可以直接用 MySQL 的索引避免字符串比较的隐式转换问题。idx_shop_time这个联合索引对时间冲突检测很关键。创建预约时冲突查询的条件会落在shop_id、start_time、end_time三个字段上这个索引能让查询不扫全表。status字段用 TINYINT 存数字业务层用枚举或常量类映射不在数据库里直接存中文状态。这样后续要加英文版、要改状态名都方便不会动数据库结构。appointment_no生成规则建议用日期加随机数或自增序列例如20250612 6位随机数。它是用户和商家沟通时的凭证最好保持稳定且可读。4. 微信小程序登录态与鉴权流程4.1 登录的误区每次写微信小程序项目都会有人问能不能直接在小程序端把openid传给后端后端拿这个openid建用户不能。openid是用户在小程序内的身份标识但它不是登录凭证。任何一个知道openid的人都可以伪造请求冒充其他用户操作数据。真正的流程是wx.login获取临时code后端拿code换取openid然后签发自己的token给小程序。后续请求都带token后端通过token识别用户身份。4.2 登录链路设计完整流程如下小程序端调用wx.login获取临时code。小程序把code通过wx.request发给后端/wx/login接口。后端拿着code请求微信的jscode2session接口换到openid和session_key。后端用openid查用户表不存在则自动注册新用户。后端生成自定义token可以是随机字符串或 JWT返回给小程序端。小程序端把token存入wx.setStorageSync后续所有请求在 header 里带上Authorization。后端通过拦截器统一校验token解析出当前用户。这里最关键的一点是AppSecret只能保存在后端服务端。如果在小程序代码里明文写AppSecret任何人反编译或抓接口都能拿到造成的风险远大于登录问题本身。4.3 后端登录接口示例// 文件路径src/main/java/com/example/petgrooming/controller/WxAuthController.java RestController RequestMapping(/wx) public class WxAuthController { Resource private WxAuthService wxAuthService; PostMapping(/login) public ResultString login(RequestBody LoginRequest request) { if (StringUtils.isBlank(request.getCode())) { return Result.fail(code不能为空); } String token wxAuthService.login(request.getCode()); return Result.success(token); } }// 文件路径src/main/java/com/example/petgrooming/service/impl/WxAuthServiceImpl.java Service public class WxAuthServiceImpl implements WxAuthService { Value(${wx.appid}) private String appid; Value(${wx.secret}) private String secret; Resource private RestTemplate restTemplate; Resource private PetUserMapper petUserMapper; Override public String login(String code) { String openid code2Session(code); PetUser user petUserMapper.selectOne( new LambdaQueryWrapperPetUser() .eq(PetUser::getOpenid, openid) .last(limit 1) ); if (user null) { PetUser newUser new PetUser(); newUser.setOpenid(openid); petUserMapper.insert(newUser); return TokenUtils.generateToken(newUser.getId()); } return TokenUtils.generateToken(user.getId()); } private String code2Session(String code) { String url String.format( https://api.weixin.qq.com/sns/jscode2session?appid%ssecret%sjs_code%sgrant_typeauthorization_code, appid, secret, code ); MapString, Object ret restTemplate.getForObject(url, Map.class); if (ret null || ret.containsKey(errcode)) { throw new BusinessException(微信登录码无效或已过期); } return (String) ret.get(openid); } }代码里用到了LambdaQueryWrapper这是 MyBatis-Plus 的封装实际项目可按团队习惯换成 XML Mapper。重点是流程code 只为一次而生换到 openid 后 code 立即失效所以后端不能缓存 code 重复使用。4.4 小程序端登录示例// 文件路径miniprogram/pages/login/login.js const request require(../../utils/request); Page({ data: { loading: false }, onLoad() { this.login(); }, login() { this.setData({ loading: true }); wx.login({ success: (res) { if (res.code) { this.exchangeToken(res.code); } else { wx.showToast({ title: 登录失败, icon: none }); } }, fail: () { wx.showToast({ title: 登录失败, icon: none }); }, complete: () { this.setData({ loading: false }); } }); }, exchangeToken(code) { request.post(/wx/login, { code }).then((data) { wx.setStorageSync(token, data.token); wx.switchTab({ url: /pages/index/index }); }).catch(() { wx.showToast({ title: 登录接口异常, icon: none }); }); } });实际项目中建议把request封装成统一模块自动在 header 里追加token并统一处理后端返回的code。这样页面代码只需要关心业务成功或失败。// 文件路径miniprogram/utils/request.js const BASE_URL https://api.example.com; function request(method, url, data, header {}) { const token wx.getStorageSync(token); if (token) { header[Authorization] Bearer token; } return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method, data, header, success: (res) { if (res.data res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: (err) reject(err) }); }); } module.exports { get: (url, params) request(GET, url, params), post: (url, data) request(POST, url, data) };这里的BASE_URL需要替换成实际部署后端后的 HTTPS 域名。小程序生产环境要求所有请求域名必须配置到微信公众平台的服务器域名白名单里且必须是 HTTPS。5. 宠物美容预约系统核心代码实现5.1 创建预约与时间冲突检测创建预约是系统中最重要的接口。它的业务规则包括用户必须登录。必须选择存在的宠物、服务项目、门店。服务结束时间 开始时间 项目预计耗时。同一门店同一时间段不能存在状态为待确认、已确认、服务中的重叠预约。时间区间重叠判断的通用公式是已有记录的开始时间 新预约的结束时间 AND 已有记录的结束时间 新预约的开始时间这个公式可以覆盖所有重叠情况完全包含、部分重叠、首尾相接等。边界条件是“新开始等于已有结束”或“新结束等于已有开始”这两种情况视为不冲突因为前后两个预约可以无缝衔接。// 文件路径src/main/java/com/example/petgrooming/service/impl/AppointmentServiceImpl.java Service public class AppointmentServiceImpl implements AppointmentService { Resource private AppointmentMapper appointmentMapper; Resource private PetProfileMapper petProfileMapper; Resource private ServiceItemMapper serviceItemMapper; Override Transactional(rollbackFor Exception.class) public Long createAppointment(AppointmentCreateDTO dto, Long userId) { // 1. 校验宠物归属 PetProfile pet petProfileMapper.selectById(dto.getPetId()); if (pet null || !pet.getUserId().equals(userId)) { throw new BusinessException(宠物信息不存在); } // 2. 校验服务项目 ServiceItem item serviceItemMapper.selectById(dto.getServiceItemId()); if (item null || item.getStatus() ! 1) { throw new BusinessException(服务项目不可用); } // 3. 计算开始结束时间 LocalDateTime start LocalDateTime.of(dto.getAppointmentDate(), dto.getStartTime()); LocalDateTime end start.plusMinutes(item.getDurationMinutes()); // 4. 冲突检测 Long conflictCount appointmentMapper.selectCount( new LambdaQueryWrapperAppointment() .eq(Appointment::getShopId, dto.getShopId()) .lt(Appointment::getStartTime, end) .gt(Appointment::getEndTime, start) .in(Appointment::getStatus, AppointmentStatus.PENDING_CONFIRM, AppointmentStatus.CONFIRMED, AppointmentStatus.SERVING) ); if (conflictCount 0) { throw new BusinessException(该时段已被预约请选择其他时间); } // 5. 保存预约 Appointment appointment new Appointment(); appointment.setAppointmentNo(generateAppointmentNo()); appointment.setUserId(userId); appointment.setPetId(dto.getPetId()); appointment.setServiceItemId(dto.getServiceItemId()); appointment.setShopId(dto.getShopId()); appointment.setAppointmentDate(dto.getAppointmentDate()); appointment.setStartTime(start); appointment.setEndTime(end); appointment.setStatus(AppointmentStatus.PENDING_CONFIRM); appointment.setRemark(dto.getRemark()); appointmentMapper.insert(appointment); return appointment.getId(); } private String generateAppointmentNo() { return DateTimeFormatter.ofPattern(yyyyMMddHHmmss).format(LocalDateTime.now()) String.format(%04d, ThreadLocalRandom.current().nextInt(10000)); } }这段代码的关键是Transactional注解。创建预约包含“查询宠物、计算时间、冲突检测、插入记录”多个步骤如果冲突检测通过后、插入记录前异常事务回滚可以避免产生脏数据。虽然这里没有跨表写入但养成事务习惯对后面的“确认预约后释放其他时间段”等扩展很重要。需要注意的是冲突检测使用selectCount只能防止单次请求内的冲突。如果系统并发量较大两个用户同时提交同一时段的预约检测和插入之间会有时间窗口。更严格的方案是在预约表上引入唯一约束或使用数据库锁但对课程设计和中小型预约场景上面的逻辑已经足够在架构层面不引入额外的复杂度。5.2 预约状态机统一流转状态机是预约系统里容易被忽略、却最能体现工程能力的设计。直接写appointment.setStatus(4)的问题在于任何人都能在任何阶段把预约改成任何状态。比如“服务中”的预约被直接改成“已完成”虽然看起来没问题但如果后续要统计“取消率”“爽约率”数据就会很乱。推荐用统一方法处理状态流转先校验再变更// 文件路径src/main/java/com/example/petgrooming/service/impl/AppointmentServiceImpl.java public void updateStatus(Long appointmentId, Long operatorId, int targetStatus) { Appointment appointment appointmentMapper.selectById(appointmentId); if (appointment null) { throw new BusinessException(预约不存在); } int currentStatus appointment.getStatus(); if (targetStatus AppointmentStatus.CANCELLED) { // 用户取消只能取消待确认或已确认的预约 if (currentStatus ! AppointmentStatus.PENDING_CONFIRM currentStatus ! AppointmentStatus.CONFIRMED) { throw new BusinessException(当前状态不允许取消); } } else if (targetStatus AppointmentStatus.CONFIRMED) { // 商家确认只能确认待确认的预约 if (currentStatus ! AppointmentStatus.PENDING_CONFIRM) { throw new BusinessException(只有待确认预约才能确认); } } else if (targetStatus AppointmentStatus.SERVING) { // 商家开始服务只能从已确认转服务中 if (currentStatus ! AppointmentStatus.CONFIRMED) { throw new BusinessException(只有已确认预约才能开始服务); } } else if (targetStatus AppointmentStatus.COMPLETED) { // 商家完成服务只能从服务中转已完成 if (currentStatus ! AppointmentStatus.SERVING) { throw new BusinessException(只有服务中预约才能完成); } } else { throw new BusinessException(不支持的状态变更); } appointment.setStatus(targetStatus); appointmentMapper.updateById(appointment); }这种做法的好处是所有状态流转规则集中在一个方法里后续要加规则只改这里。调用方不需要理解完整业务背景只需表达“我要取消”“我要确认”。方便加操作日志。可以在方法里追加一条操作记录写清楚谁在什么时间把状态从几改成了几。状态枚举可以用简单常量类也可以用枚举加强类型校验// 文件路径src/main/java/com/example/petgrooming/constant/AppointmentStatus.java public final class AppointmentStatus { private AppointmentStatus() { } public static final int PENDING_CONFIRM 0; public static final int CONFIRMED 1; public static final int SERVING 2; public static final int COMPLETED 3; public static final int CANCELLED 4; public static final int NO_SHOW 5; }实际项目如果使用的是 Java 17 及以上可以改用 enum 替代常量但常量版本已经足够清晰也方便存入数据库做数字映射。5.3 小程序端预约表单页面预约页面的交互是选择宠物、选择服务项目、选择日期、选择开始时间、填写备注、点击提交。!-- 文件路径miniprogram/pages/appointment/appointment.wxml -- view classappointment-page picker modeselector range{{petNames}} bindchangeonPetChange view classfield宠物{{selectedPetName || 请选择宠物}}/view /picker picker modeselector range{{serviceNames}} bindchangeonServiceChange view classfield服务项目{{selectedServiceName || 请选择服务项目}}/view /picker picker modedate start{{today}} bindchangeonDateChange view classfield日期{{appointmentDate || 请选择日期}}/view /picker picker modetime bindchangeonTimeChange view classfield开始时间{{startTime || 请选择开始时间}}/view /picker textarea placeholder备注选填 bindinputonRemarkInput/textarea button typeprimary bindtaponSubmit loading{{submitting}}提交预约/button /view// 文件路径miniprogram/pages/appointment/appointment.js const request require(../../utils/request); Page({ data: { pets: [], petNames: [], selectedPetId: 0, serviceItems: [], serviceNames: [], selectedServiceId: 0, today: , appointmentDate: , startTime: , remark: , submitting: false }, onLoad() { const now new Date(); const year now.getFullYear(); const month String(now.getMonth() 1).padStart(2, 0); const day String(now.getDate()).padStart(2, 0); this.setData({ today: ${year}-${month}-${day} }); this.loadPets(); this.loadServiceItems(); }, async loadPets() { const data await request.get(/pet/list); this.setData({ pets: data, petNames: data.map(item item.name) }); }, async loadServiceItems() { const data await request.get(/service/list); this.setData({ serviceItems: data, serviceNames: data.map(item ${item.name}${item.price}元 / ${item.durationMinutes}分钟) }); }, onPetChange(e) { const index Number(e.detail.value); this.setData({ selectedPetId: this.data.pets[index].id, selectedPetName: this.data.pets[index].name }); }, onServiceChange(e) { const index Number(e.detail.value); this.setData({ selectedServiceId: this.data.serviceItems[index].id, selectedServiceName: this.data.serviceItems[index].name }); }, onDateChange(e) { this.setData({ appointmentDate: e.detail.value }); }, onTimeChange(e) { this.setData({ startTime: e.detail.value }); }, onRemarkInput(e) { this.setData({ remark: e.detail.value }); }, async onSubmit() { if (!this.data.selectedPetId) { wx.showToast({ title: 请选择宠物, icon: none }); return; } if (!this.data.selectedServiceId) { wx.showToast({ title: 请选择服务项目, icon: none }); return; } if (!this.data.appointmentDate || !this.data.startTime) { wx.showToast({ title: 请选择时间, icon: none }); return; } this.setData({ submitting: true }); try { await request.post(/appointment/create, { petId: this.data.selectedPetId, serviceItemId: this.data.selectedServiceId, shopId: 1, appointmentDate: this.data.appointmentDate, startTime: this.data.startTime, remark: this.data.remark }); wx.showToast({ title: 预约成功等待确认, icon: success }); setTimeout(() { wx.switchTab({ url: /pages/order/order }); }, 1500); } catch (e) { wx.showToast({ title: 预约失败, icon: none }); } finally { this.setData({ submitting: false }); } } });页面代码遵循了“先校验再提交”的原则。前端校验是为了提升体验后端必须做完整校验因为请求可以被直接构造。前端setTimeout跳转是为了让用户看到成功提示真实项目里也可以直接跳转不延迟。5.4 预约列表按状态筛选用户预约列表的常见交互是顶部 tab 切换全部、待确认、进行中、已完成。后端接口可以接收status参数为 0 时返回所有状态否则按具体状态过滤。// 文件路径src/main/java/com/example/petgrooming/controller/AppointmentController.java GetMapping(/list) public ResultListAppointmentVO list( RequestParam(required false) Integer status, RequestParam(required false) Integer pageNum, RequestParam(required false) Integer pageSize) { Long userId CurrentUserHolder.getUserId(); ListAppointmentVO list appointmentService.listByUser(userId, status, pageNum, pageSize); return Result.success(list); }这里使用CurrentUserHolder从请求上下文中取出当前登录用户而不是让前端传userId。原因和前面说的一样不能信任前端传入的用户标识必须从token中解析身份。6. 运行结果与效果验证6.1 启动后端后端项目使用 Maven 构建启动命令mvn spring-boot:run启动成功后控制台会出现 Spring Boot 启动日志例如 Tomcat started on port 8080。接着可以用 POST 请求测试登录接口curl -X POST http://localhost:8080/wx/login \ -H Content-Type: application/json \ -d {code: test-code}注意真正调试时code必须来自微信wx.login返回的临时凭证手动填写的无效 code 会在后端调用微信接口时返回错误。本地开发如果还没有配置真实 AppID可以使用小程序的测试号或在后端把code2Session方法临时加一个开发态判断直接根据 code 生成 mock openid。上线前必须切回真实逻辑。6.2 微信开发者工具运行小程序打开微信开发者工具选择“导入项目”目录指向小程序前端目录。填入测试 AppID或使用测试号。在“详情 - 本地设置”中勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。编译项目模拟器中会自动触发登录流程。进入预约页面选择宠物、服务、时间点击提交。预期结果是页面弹出“预约成功等待确认”后端数据库中appointment表新增一条记录status字段为 0。如果预约同一门店同一时段第二次提交相同时间页面会提示“该时段已被预约请选择其他时间”。6.3 验证状态流转用商户账号访问管理端确认接口后再次查询预约记录status应从 0 变为 1。通过数据库查询验证最直接SELECT id, appointment_no, status, start_time, end_time FROM appointment ORDER BY id DESC LIMIT 5;如果状态没有变化优先检查后端日志中是否有异常以及调用的接口路径是否命中了正确的 Controller 方法。7. 常见问题与排查方法问题现象可能原因排查方式解决方案小程序请求后端接口报 fail未配置合法域名或本地未勾选“不校验合法域名”打开开发者工具 Network 面板查看请求状态开发者工具勾选“不校验合法域名”生产环境配置 HTTPS 合法域名模拟器正常真机白屏或页面无数据基础库兼容性、ES6 语法未转 ES5、请求域名未备案真机调试看 console 报错开启 ES6 转 ES5确认所有请求域名已配置为合法域名登录提示 code 无效code 只能使用一次重复使用会失效AppSecret 配置错误查看后端日志中微信接口返回的 errcode每次登录都应重新调用 wx.login 获取新 code核对服务端 appid/secret时间段被重复预约冲突检测的区间判断条件写反打印最终执行的 SQL检查条件使用区间重叠公式已有 start 新 end 且 已有 end 新 start点击提交按钮没有反应前端校验未通过、事件绑定错误、请求未发出在 onSubmit 第一行加 console.log确认事件被触发检查 bindtap 是否正确逐项检查必填字段校验预约状态乱跳状态变更没有走统一入口直接在业务代码中 setStatus检查代码中所有 setStatus 的调用位置收敛为统一状态流转方法并在方法内做状态前置校验取消预约后原来时间仍不能预约冲突检测没有排除已取消状态检查冲突检测 SQL 中的 status 条件只统计待确认、已确认、服务中三种状态8. 最佳实践与工程建议8.1 状态机收敛到统一入口不要在 Controller、Service、定时任务里分别写状态赋值逻辑。所有状态变更统一走一个方法内部先校验再修改。这样业务规则清晰后期加“操作日志”也只需要改一处。8.2 时间处理统一使用时间戳或标准 DATETIME前端传日期和时间时后端用LocalDateTime直接解析拼接避免用字符串拼接后比较。数据库中start_time、end_time使用 DATETIME并建立联合索引让冲突检测查询走索引。8.3 接口权限最小化用户只能操作自己的宠物和预约记录后端必须在 service 层校验归属关系。管理端接口需要单独的鉴权方式不能和普通用户共用一套token逻辑。最简单的做法是在用户表中增加role字段管理端接口校验role ADMIN。8.4 定时任务处理超时未确认预约真实门店场景中用户提交预约后如果商家长时间不确认这个时间段会被一直占住。可以在后端加一个定时任务把超过一定时间仍未确认的预约自动置为已取消并释放时间段。定时任务同样走统一状态流转方法不能直接改状态。// 示意每天凌晨执行一次取消超过24小时未确认的预约 Component public class AppointmentTimeoutTask { Resource private AppointmentService appointmentService; Scheduled(cron 0 0 2 * * ?) public void cancelTimeoutAppointments() { appointmentService.cancelTimeoutAppointments(24); } }8.5 上线顺序和审核提醒小程序提审前后端必须已经部署到正式服务器域名完成备案并配置 HTTPS微信公众平台的服务器域名白名单也要提前配置好。顺序上先部署后端和配置域名再提交小程序代码审核。审核人员打开小程序时会真实调用接口如果后端还在本地审核必然失败。8.6 不要过早引入复杂设计预约系统这类项目不需要一上来就引入 Redis 分布式锁、消息队列、分库分表。先把状态机、冲突检测、权限校验做严谨比堆积中间件更能体现工程能力。性能优化等到真实有并发压力时再做这个时间点大概率不会出现在课程设计阶段。9. 总结与后续学习方向这篇文章围绕宠物美容预约系统重点讲了三个核心设计预约状态机的统一流转、时间冲突检测的区间判断公式、微信小程序登录态的正确链路。同时还给出了用户表、宠物表、服务项目表、预约记录表的表结构设计以及前端预约表单的完整示例代码。预约系统的竞争力不在于界面多漂亮而在于状态流转是否严谨、时间冲突是否可控、异常数据是否能恢复。建议先把最小闭环跑通登录、创建预约、冲突检测、商家确认、用户查看列表。这个闭环稳定之后再考虑订阅消息通知、取消原因统计、门店和美容师管理、后台数据报表等扩展功能。下一步可以重点研究微信订阅消息的一次性模板能力把“预约成功”“预约被拒绝”“服务开始提醒”串成完整的消息闭环。也可以学习如何为管理端单独做一个 Web 管理后台复用同一套后端接口这是很多真实预约系统的最终形态。建议把本文的设计思路收藏备用动手实现时对照状态流转和冲突检测这两块重点测试。
返回列表