简介:一套基于Java SSM框架与uniapp/原生小程序开发的理发店预约系统毕业设计源码,面向需要完成课程设计或毕业设计的计算机专业学生,聚焦理发店客户预约、服务项目管理与会员维护等实际业务场景。系统后台涵盖预约信息管理、理发信息管理、会员信息管理、系统设计管理,可处理预约时间、理发师分配、项目价格调整、会员消费记录与反馈;小程序端包含首页、理发项目、理发师、我的等模块,前后端数据联动完整,能够体现真实商业项目中的权限划分与流程控制。资源共416个文件,压缩包约36.21MB,以Java源码与编译后class文件、SSM框架配置、小程序页面wxml/wxss/js、数据库SQL脚本、项目说明文档为主,附带LW资料;其中说明文档梳理设计思路,SQL脚本便于快速搭建数据库,预约、评价等业务控制类覆盖核心接口流程。目前已有165人学习下载,适合作为Java与小程序全栈开发的完整项目范例,可直接用于课题参考与二次开发。
1. 小程序理发店预约系统:一份不用改架构就能跑的毕业设计源码
这份小程序理发店预约系统的完整源码包里,前端是 uniapp 工程,后端是 SSM(Spring + SpringMVC + MyBatis)服务,配了 mysql 5.7 建表脚本、一份说明文档和论文(LW),属于那种打开就能看到完整业务闭环的毕业设计。它解决的是服务行业最典型的预约场景:用户在小程序里浏览理发项目、选理发师、提预约,管理员在后台受理并维护会员数据。适合两类人:一是做 Java 或小程序方向毕业设计、想找一个能讲清楚业务链路的项目来二次开发的学生;二是想快速搭一套预约系统原型、验证小店运营流程的开发者。先说结论:这项目的骨架非常标准,但有几个环境上的坑,照着文末的排查清单能省大量时间。
2. 后端 SSM 结构:从 class 文件反向梳理 Controller 层与 DAO 层的分工
拿到压缩包后先别急着跑起来,建议先看文件列表里的 class 文件名。这份源码虽然没有按传统方式把包结构完整列出来,但从类名能判断出整个后端的模块边界,这比直接看代码更高效。
2.1 从编译后的类名反推 Controller 层的拆分思路
文件列表里出现了五个 Controller 类:ApiController、AutoCodeController、HaircutAppointmentController、BarberAppointmentController、HaircutEvaluateController、BarberEvaluateController,还有一个BaseController。这个命名方式是 SSM 项目里很典型的写法:一个基类统一处理通用逻辑,其余 Controller 按业务实体拆分。
BaseController通常承担两类职责:一是注入HttpServletRequest、HttpServletResponse或HttpSession,让子类直接取参数;二是定义一个统一的 JSON 返回结构。很多 SSM 毕业设计里,这个基类里会有一个writeJson(Object data)方法,把HashMap转成 JSON 字符串写回前端。常见做法是返回{ "code": 200, "msg": "成功", "data": ... }这样的三元组,小程序端拿code判断请求是否成功,拿data渲染列表。
// BaseController.java 典型写法,这套源码里大概率也是这个结构 public class BaseController { // 统一返回结构:code=200 表示成功,非200表示业务错误 protected void writeJson(HttpServletResponse response, int code, String msg, Object data) throws IOException { response.setContentType("application/json;charset=utf-8"); Map<String, Object> result = new HashMap<String, Object>(); result.put("code", code); result.put("msg", msg); result.put("data", data == null ? new Object() : data); response.getWriter().write(new ObjectMapper().writeValueAsString(result)); } }这段代码的逻辑是:所有 Controller 继承BaseController后,调用writeJson就能把结果统一格式输出。ObjectMapper来自 Jackson,是 SSM 项目中最常用的 JSON 序列化工具;charset=utf-8必须显式声明,否则返回中文会乱码。参数里的code通常定义为常量,比如 200 代表正常、500 代表服务器异常,小程序端只需要判断这一个字段。
2.2 ApiController 与 AutoCodeController:对外接口和代码生成器
ApiController一般是所有小程序请求的入口聚合。实际项目中常见做法是:ApiController上标注@RequestMapping("/api"),方法按模块继续细分,比如/api/haircut/list返回理发项目列表、/api/appointment/add接收预约提交。这样前端只需要记住一个根路径,后端路由也不会乱。
AutoCodeController这个名字值得单独说,它是整个项目里性价比最高的部分。很多 SSM 毕业设计里会带一个根据数据库表结构自动生成 Controller、Service、DAO、Mapper XML 的代码生成器,AutoCodeController就是暴露这个能力的控制器。它的典型逻辑是:接收一个表名参数,然后读取数据库连接里该表的元数据,用字符串模板拼接出标准的增删改查代码,再写回项目目录。
// AutoCodeController 生成逻辑的简化示意 @Controller @RequestMapping("/auto") public class AutoCodeController extends BaseController { @RequestMapping("/generate") public void generate(HttpServletRequest request, HttpServletResponse response) throws Exception { String tableName = request.getParameter("tableName"); // 读取表结构:查询 information_schema.columns 获取字段名和类型 List<Map<String, Object>> columns = dao.queryColumns(tableName); // 根据字段拼装 entity 类属性 StringBuilder entityCode = new StringBuilder(); for (Map<String, Object> col : columns) { String fieldName = col.get("columnName").toString(); // 下划线转驼峰 // 拼接 private 字段声明与 getter/setter } // 写文件到 src/main/java 对应包路径 // 同时生成 Mapper 接口与 Mapper.xml 的增删改查语句 } }这个类存在的意义是:当你需要新增一个业务表(比如增加一个"发型师等级表"),不用手写整套 DAO 和 Service,直接在浏览器里访问一次/auto/generate?tableName=xxx,代码就生成完了。参数tableName对应数据库里存在的物理表名,生成前必须确认表已经通过 Navicat 或 SQL 脚本创建好,否则读取不到字段定义。这套源码把生成器放进 Controller 而不是独立工具类,说明作者在设计时考虑了 Web 端触发的便利性,也算是一个答辩亮点。
2.3 Haircut 与 Barber 两套预约逻辑为什么会分开
文件列表里最值得玩味的是预约拆分成了两个 Controller:HaircutAppointmentController和BarberAppointmentController。一开始我以为是冗余,但结合摘要里提到的"后台预约信息管理、理发信息管理、会员信息管理"来看,这是两种预约维度。
一种维度是按理发项目预约:用户先在产品页选中"剪发 68 元",再选一个时间提交;后台记录的重点是"哪个项目在什么时间被预约了"。另一种维度是按理发师预约:用户指定某个理发师,后台需要记录该理发师的时间段占用情况。
在代码层的体现是两张预约表,字段侧重点不同。
// HaircutAppointmentController 的项目维度预约 @RequestMapping("/haircutAppoint") public void haircutAppoint(HttpServletRequest request, HttpServletResponse response) throws Exception { String memberId = request.getParameter("memberId"); // 会员ID String itemId = request.getParameter("itemId"); // 理发项目ID String appointTime = request.getParameter("appointTime"); // 预约时段 // 校验会员是否存在、项目是否在售、时段是否冲突 // 插入预约记录,状态默认 0(待受理) writeJson(response, 200, "预约成功", null); } // BarberAppointmentController 的理发师维度预约 @RequestMapping("/barberAppoint") public void barberAppoint(HttpServletRequest request, HttpServletResponse response) throws Exception { String memberId = request.getParameter("memberId"); String barberId = request.getParameter("barberId"); // 理发师ID String appointTime = request.getParameter("appointTime"); // 检查该理发师在 appointTime 是否已被占用 // 插入预约记录并标记 barberId writeJson(response, 200, "预约成功", null); }参数上最大的区别是itemId和barberId,这决定了后台的排班逻辑。实际改造项目时,如果你想做一个"项目 + 理发师 + 时段"三选一的复杂预约,需要把这两张表合并或建立关联,当前拆开的做法更适合入门和答辩演示。对新手来说,先照原样跑通两个接口,搞懂两种预约的数据流,比强行合并更有价值。
2.4 BaseDAO、DateUtil、StringUtil 的配合方式
BaseDAO出现得很及时,它说明项目里有一个所有 DAO 的公共父类,通常封装了增删改查四个基础方法的公共实现,子类只需要写上自己的特殊 SQL。DateUtil和StringUtil是工具类,SSM 项目里它们一般做这些事:DateUtil负责把前端传来的字符串时间解析成java.util.Date(比如"2024-05-20 14:00"转成时间对象存库),StringUtil负责做isEmpty判断、手机号脱敏、随机订单号生成。预约系统的核心逻辑正是靠它们处理时间冲突和空值校验的。
3. mysql 5.7 数据库设计:预约、会员、评价三张核心表的字段与关系
数据库在整套源码里的地位不亚于后端代码。压缩包里单独的数据库文件夹,一般包含建库脚本、建表脚本和初始化数据脚本。这一章聊清楚表结构设计和导入顺序。
3.1 数据库文件夹的导入顺序与核对方法
拿到脚本后,不要一上来就全选执行。常见做法是手工建一个库,再按依赖顺序导入。Navicat 11 里操作时,先新建连接,字符集选 utf8,然后执行建库和建表语句。
# 通过 Navicat 新建查询,先建库再刷数据 CREATE DATABASE IF NOT EXISTS barber_appointment DEFAULT CHARSET utf8mb4; USE barber_appointment; # 然后依次执行 barber_appointment.sql 中的建表语句 # 最后执行 init_data.sql 补基础数据(理发项目、理发师账号)utf8mb4不是随手选的,mysql 5.7 里它支持完整的 Unicode 字符集,小程序端用户昵称可能包含 emoji,用utf8mb4可以避免入库报错。初始化数据里通常会预置几条理发项目和理发师记录,这是小程序端首页能渲染出内容的依赖,如果直接跳过初始化,前端打开就是空白。
3.2 预约记录表的字段设计与状态流转
预约表是整个系统的数据核心,这里以常见的设计为例,结构上大概率包含预约 ID、会员 ID、理发师 ID、项目 ID、预约时间、状态字段、备注。
CREATE TABLE `appointment` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '预约ID', `member_id` int(11) DEFAULT NULL COMMENT '会员ID,关联member表', `barber_id` int(11) DEFAULT NULL COMMENT '理发师ID,关联barber表', `item_id` int(11) DEFAULT NULL COMMENT '理发项目ID,关联haircut_item表', `appoint_time` datetime DEFAULT NULL COMMENT '预约的具体时间', `status` tinyint(4) DEFAULT '0' COMMENT '0待受理 1已完成 2已取消', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `remark` varchar(255) DEFAULT NULL COMMENT '用户备注', PRIMARY KEY (`id`), KEY `idx_member` (`member_id`), KEY `idx_barber_time` (`barber_id`, `appoint_time`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;status字段是业务流转的关键,后端 Controller 里受理和取消都靠更新它。idx_barber_time这个联合索引很有参考价值,它让"查某个理发师在某个时间段是否被占用"的 SQL 走索引而不是全表扫。预约系统的并发量不大,但时间范围查询特别频繁,这个索引是必须的。AUTO_INCREMENT=1只是初始值,实际运行后会自动递增。
3.3 会员表与评价表的关联设计
会员表记录基础信息和小程序登录标识,评价表则承载用户对理发项目和理发师的反馈。文件列表里出现了HaircutEvaluateController和BarberEvaluateController,对应两张评价表或一张带类型字段的评价表。
CREATE TABLE `member` ( `id` int(11) NOT NULL AUTO_INCREMENT, `nickname` varchar(50) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `openid` varchar(64) DEFAULT NULL COMMENT '微信小程序openid', `balance` decimal(10,2) DEFAULT '0.00', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `evaluate` ( `id` int(11) NOT NULL AUTO_INCREMENT, `member_id` int(11) NOT NULL, `evaluate_type` tinyint(4) DEFAULT '0' COMMENT '0=理发项目 1=理发师', `target_id` int(11) NOT NULL COMMENT '项目ID或理发师ID', `score` tinyint(4) DEFAULT '5' COMMENT '1到5分', `content` varchar(500) DEFAULT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;evaluate_type + target_id的组合让一张评价表同时服务两个 Controller,是省表的常见做法。target_id的含义由evaluate_type决定,这种设计比较简洁,但写 SQL 时要注意区分。如果你在后端要展示"某理发师的平均分",一条聚合查询就能完成。score用tinyint而不是int,是为了省存储并限制取值范围,这也是做表设计时常被忽略的细节。
3.4 理发项目表与理发师表的初始化数据
理发项目表和理发师表是小程序首页的内容来源。项目表至少要包含项目名称、价格、时长、描述、图片路径;理发师表要包含姓名、头像、级别(总监/高级/初级)、擅长项目。初始化数据里一般会塞 4 到 6 条项目、3 到 5 位理发师,保证小程序端打开就有内容可看。
4. uniapp 小程序端:接口联调、页面渲染与登录态处理的完整链路
小程序端用的是 uniapp 框架,源码里应该是标准的pages目录结构:pages/index/index对应首页、pages/haircut/haircut对应理发项目列表、pages/barber/barber对应理发师列表、pages/mine/mine对应个人中心。后端接口对接在utils/request.js或api.js里统一封装。
4.1 uniapp 页面与后端接口的映射关系
先把页面路由和后端接口在脑子里建立一张对应表,再动手改代码。首页会请求项目列表和热门理发师,项目页会请求项目详情和评价,理发师页请求理发师列表,提交预约走预约接口,"我的"页面请求会员信息和预约记录。这种映射关系理顺了,小程序端从哪里取数就一目了然。
4.2 uni.request 的二次封装:baseUrl 与 token 注入
这份源码的小程序端大概率在utils/request.js里封装了请求方法,核心是给所有请求统一加 baseUrl 和 token。实际开发中我更推荐你自己写一遍这个封装,因为毕业设计答辩时老师大概率会问"接口地址在哪里统一配置的"。
// utils/request.js 典型封装方式 const BASE_URL = 'http://localhost:8080/barber_appointment/api'; function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/x-www-form-urlencoded', 'token': uni.getStorageSync('token') || '' }, success: (res) => { if (res.statusCode === 200) { resolve(res.data); // 后端 BaseController 统一的 JSON 结构 } else { reject(res); } }, fail: (err) => reject(err) }); }); } export default request;这段封装的三个关键点:BASE_URL必须和后端项目部署地址一致,默认写localhost只在模拟器上有效,真机调试必须改成电脑的局域网 IP;token从本地缓存读取,每次请求带上,后端才能识别当前用户;method默认 GET,提交预约和登录接口需要显式传POST。代码里Content-Type用的是表单格式,后端request.getParameter能直接取到值,如果改成application/json,后端提交方式就要跟着变。
4.3 微信小程序登录态的处理
摘要里写明了小程序端有"我的"功能模块,登录方式在原生小程序里通常是wx.login换取code,后端再拿code去微信接口换openid,然后生成自己的登录凭证。uniapp 工程里这个逻辑被封装成了uni.login。常见的简化做法是:后端把openid作为会员的唯一标识,首次登录自动注册,后续登录直接返回用户信息。
// pages/mine/mine.vue 登录按钮的处理逻辑 uni.login({ provider: 'weixin', success: (loginRes) => { uni.request({ url: BASE_URL + '/login/wxLogin', method: 'POST', data: { code: loginRes.code }, success: (res) => { if (res.data.code === 200) { uni.setStorageSync('token', res.data.data.token); uni.setStorageSync('userInfo', res.data.data.member); } } }); } });这里的code是一次性的,后端换openid后就不能再重复使用。调试时最容易翻车的点是后端appid和secret没配置,或者配置成了别人的小程序账号,导致换取openid失败。如果你不是做完整微信登录,只是本地演示,可以让后端临时返回一个固定的测试 token,把登录逻辑跳过去,等答辩前再开通正式的小程序账号。
4.4 HBuilder X 打包到微信开发者工具的注意事项
运行这套源码时,先确认 HBuilder X 里是否已导入项目,然后右键项目选择"运行到小程序模拟器 → 微信开发者工具"。如果微信开发者工具没有自动打开,常见原因是工具的服务端口没开,需要在微信开发者工具的"设置 → 安全设置"里打开服务端口。每次改了后端接口,小程序端不生效,多半是重新编译后的代码没有同步到微信开发者工具,先删掉dist/dev目录再重新运行。
5. 环境搭建与常见问题排查:tomcat7、JDK1.8、mysql 5.7 的坑位清单
这一章直接进入实战。压缩包对应的环境是 JDK1.8 + tomcat7 + mysql 5.7 + Navicat11,这套组合在 2024 年的电脑上跑通,有几个高频坑要提前知道。以下都是实际操作中一定会遇到的。
5.1 JDK 版本与编译级别不一致:启动报 UnsupportedClassVersionError
现象:tomcat 启动时报UnsupportedClassVersionError: ... Unsupported major.minor version 52.0。
原因:major.minor version 52.0对应的是 JDK 1.8 编译出来的 class 文件。如果本地安装的 JDK 是 1.7 甚至更低,tomcat 加载 class 时直接抛异常。这类源码包通常是别人用 JDK1.8 的 eclipse 编译好的,你的运行环境必须也是 1.8。
解决:先java -version确认 JDK 版本。如果不是 1.8,从 Oracle 官网装一个 JDK8,然后在 eclipse 或 IDEA 的Java Compiler设置里把编译级别固定为 1.8,再重新导出项目。如果你用的是 IDEA,把Project Structure → Project SDK切到 1.8,Settings → Build → Compiler → Java Compiler的字节码版本也同步改到 8。
5.2 数据库导入中文乱码:问号和空心方块
现象:Navicat 里表数据正常,但小程序端列表显示???,或者数据库里的中文直接变成问号。
原因:脚本文件本身是 utf8 编码,但 Navicat 连接或导入时用了默认的 latin1 字符集。mysql 5.7 的character_set_server默认可能是 latin1,导致中文字符写入失败。
解决:在 Navicat 中右键连接属性,切到"高级"标签,将编码选为utf8;导入 SQL 文件时,在执行前执行一句SET NAMES utf8mb4;。如果已经导入了乱码数据,最快的处理方式是删库重建,不要在乱码数据上做修复,很容易越修越乱。
5.3 小程序真机预览连不上后端接口
现象:微信开发者工具里模拟器能正常显示数据,但在手机上预览时页面空白,请求超时。
原因:开发环境是http://localhost:8080,这个地址在模拟器里指向电脑本机,但在手机上指向手机自己,肯定连不上。
解决:把utils/request.js里的BASE_URL改成电脑的局域网 IP,比如http://192.168.1.101:8080/barber_appointment/api,手机和电脑连同一个 Wi-Fi。然后在微信开发者工具的"详情 → 本地设置"里勾选"不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书",因为 http 协议在正式环境是不被允许的,本地调试这一步必须跳过校验。
5.4 mysql 时区报错:The server time zone value 'Öйú±ê׼ʱ¼ä'
现象:tomcat 启动或首次访问接口时报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,界面还带乱码。
原因:mysql 5.7 以上的 JDBC 驱动默认要求指定时区,而数据库服务器的时区设置无法被自动识别。
解决:打开项目的jdbc.properties,在连接 URL 末尾追加时区和 SSL 参数:
jdbc.url=jdbc:mysql://localhost:3306/barber_appointment?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai这段配置里useUnicode=true&characterEncoding=utf8解决中文问题,useSSL=false避免连接时的 SSL 握手警告,serverTimezone=Asia/Shanghai明确告诉驱动使用东八区时间,三组参数一次性加齐。注意这套源码是基于 mysql 5.7 的,如果你本机装的是 mysql 8.0,驱动版本需要换,不推荐直接用 8.0 的默认驱动跑这个老项目,容易报Public Key Retrieval is not allowed。
5.5 tomcat7 的 URIEncoding 设置影响中文参数
现象:小程序端提交的预约备注里含中文,后端request.getParameter("remark")拿到的是乱码。
原因:tomcat7 默认的 URI 编码不是 utf8,GET请求的中文参数在传输层就坏了,POST请求的乱码则通常是后端request.setCharacterEncoding没设置。
解决:在conf/server.xml的 Connector 节点加URIEncoding="UTF-8",同时在后端项目里配置 SpringMVC 的CharacterEncodingFilter,强制请求和响应的编码都为 utf8。两个点必须同时做,只改一处会漏掉另一条链路。
6. 从登录到评价闭环:用一条业务链路验证整套系统
源码工程到底通不通,光看能不能启动是不够的。我每次拿到这类项目,都会强制自己走一遍业务链路,确认关键环节真实可用,而不是只停留在"页面能打开"。
6.1 验证链路:预约 → 受理 → 评价
第一步模拟用户操作,在小程序端完成一次完整的预约。项目源码里如果没有现成账号,可以在后台的会员表直接加一条测试记录,拿到member_id后拉起登录。提交预约后,去后台管理页面确认预约记录的状态是0(待受理)。第二步把状态更新为1(已完成),这一步在后台管理界面操作即可,本质是一条 UPDATE SQL。第三步回到小程序端,对这次服务提交评价,再去evaluate表确认记录写入。链路走通了,整个项目的主干才算真正可用。
6.2 用一条 SQL 验证评价对预约的引用
预约和评价的关联没有用外键约束,这是 SSM 项目的常规选择,避免插入数据时被外键拦住。验证时不看页面,直接用 SQL 看数据关联更直观:
-- 查看某个会员的预约记录和对应评价 SELECT a.id AS appoint_id, a.appoint_time, e.score, e.content FROM appointment a LEFT JOIN evaluate e ON e.member_id = a.member_id AND e.target_id = a.item_id WHERE a.member_id = 1;这个查询会同时返回预约信息和用户对项目的评价,如果e.score为 NULL,说明评价还没提交或者target_id关联错了。这套设计没有外键,靠应用层代码去控制两表的写入顺序,好处是删数据方便,坏处是如果评价提交时拿错了 ID,查出来就是 NULL。排查时从这条 SQL 入手比看前后端代码更快。
6.3 启动顺序和检查清单
部署这套源码最基本的检查清单是五个:mysql 5.7 服务已启动、jdbc.properties的账号密码正确、tomcat7 没有端口冲突、项目里已经部署了打包好的 war 包、微信开发者工具的服务端口已打开,按这个顺序逐项确认。曾经我帮人排查过半小时,最后发现问题只是 tomcat 的 8080 端口被占用,换到 8081 一切都好。从那以后我每次部署这套源码,都会先确认jdbc.properties和端口占用,再走一遍预约到评价的链路,花不了十分钟,但能避免大量无效调试。希望帮到你。
本文还有配套的精品资源,点击获取