简介:面向计算机专业学生的一套微信小程序端与 Java 后端联动的美容院管理系统毕业设计成果,随包提供完整源码、数据库脚本和说明文档,可直接用于毕业设计、课程设计或项目实战训练。系统围绕美容院日常运营管理场景展开,适合需要从零至一完成项目搭建、前后端联调与答辩展示的开发者,重点体现小程序端业务流程与后端服务接口的配合。压缩包共1404个文件,大小约15.86MB,核心类型包含java后端源码、vue管理后台页面、js业务逻辑、png界面素材、json配置、wxss/wxml小程序页面以及sql数据库脚本,覆盖后端接口、管理端和小程序端的完整链路。预览中还包含安装、运行、构建相关批处理脚本,便于快速启动本地开发环境;说明文档和常用配置也可用于二次修改与课设/毕设材料整理。已有318人学习/下载,对希望获取可落地参考方案、节省开发时间并理解项目结构的同学很有价值。
1. 美容院管理系统用微信小程序加Java后端,到底是解决什么问题
“美容院管理系统”是毕业设计里常出现的题目:会员开卡、储值余额、项目预约、消费记录,业务全是CRUD,却又覆盖了真实门店的核心经营场景,规模正好卡在一个学期能做完、答辩时不至于被问倒的边界上。标题里的“微信小程序+java后端”早把分工定死:小程序管顾客端自助操作,Java后端管业务规则和数据存取,中间走HTTP接口。整套系统的难点不在某个框架的新API,而在“前端传参、后端校验、数据库落地”这条链路能否一次跑通。下面从业务建模讲起,拆到建表、后端接口、小程序联调,最后列几个真机上让人半夜改代码的坑,目标是让你能复现,也能给答辩老师讲清楚每一步为什么这么做。
2. 先把架构看清:小程序、后端、数据库各自管什么
2.1 一套规范的工程目录长什么样,先按依赖顺序启动
拿到工程第一步不是找“运行按钮”,而是看目录结构。这类毕设最常见的组织方式是把小程序前端、后端Maven工程、SQL脚本、说明文档分成四个独立部分:
beauty-miniapp/ # 微信小程序端 pages/ utils/ app.js app.json beauty-server/ # Java后端,Spring Boot工程 src/main/java/ src/main/resources/ pom.xml beauty.sql # 数据库初始化脚本 毕业设计说明书.docx # 论文与说明文档核心就一条:前端、后端、SQL相互独立,别混在一个目录里。首次跑通的顺序按依赖自下而上:先执行beauty.sql创建数据库和表,再启动Spring Boot让后端连上库,最后用微信开发者工具打开miniapp目录。
# 顺序执行:先导库,再起后端 mysql -u root -p < beauty.sql cd beauty-server && mvn spring-boot:run顺序反了会出现“页面在转圈,后端日志却没动静”的假象。先导库再起后端的价值在于,后端启动失败时错误信息是明确的“拒绝连接”或“表不存在”,而不是一串嵌套异常。不少新手把三步一口气做完,接口不通时根本分不清是SQL脚本没执行成功、后端端口被占、还是小程序的域名没配,排查成本直接翻倍。
2.2 为什么是微信小程序加Java,而不是Vue加App
选型在毕业设计里要能自圆其说。微信小程序的核心优势是没有安装成本,顾客在微信里搜索或扫码就能打开,做完预约直接关掉,入口天然贴合美容院这类低频到店消费场景。相比自建App还要过应用商店审核,小程序的学习成本低得多,前端页面也只有几屏,不需要复杂路由和状态管理。
Java后端选择Spring Boot的理由更直接:自动配置省去大量XML配置,一个注解就能启动Web服务。同样的接口量,用Servlet时代的老写法要多写两倍样板代码。MySQL继续用是因为会员、订单、预约的数据模型天然是关系型的,直接建orders表存用户ID、项目ID、金额、时间,后续做月度营收统计、会员余额对账都靠SQL完成,不必引入额外中间件。硬换成文档数据库才是给自己挖坑——你要的是一张t_order表,不是一个散装JSON集合。如果嫌MyBatis注解麻烦,后期换MyBatis-Plus只会更省事,这条放到最后一章讲。
2.3 接口返回格式:联调前先把约定写进说明文档
小程序请求后端,最怕每个接口返回结构都不一样。有的接口直接返回数组,有的包一层success,有的把错误信息放在message里,前端每个页面都要写一套解析逻辑,联调越到后面越痛苦。常见做法是后端全局统一一个返回结构:
{ "code": 0, "data": { "token": "abc123", "memberId": 1001 }, "msg": "success" }约定三句话讲清楚:code为0表示成功,非0表示业务失败;data放业务数据;msg放给人看的提示。后端写一个全局异常处理器兜底,凡是业务代码没捕获的异常统一返回code为500的JSON,不把HTML错误页抛给小程序。这条约定建议写进说明文档第一页,答辩老师往往先翻文档,再对照页面操作。前后端同时遵守这个结构,联调期能少改一大半代码。
2.4 pom.xml依赖:版本号别照抄老帖子
后端工程的pom.xml是第一个配置坑。线上老帖子的依赖版本经常互相冲突,直接复制会出现“包冲突”“类不存在”这类难查的编译错误。一个最低可跑的依赖集大概是这样:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>依赖管理的逻辑是:spring-boot-starter-parent的版本一旦确定,starter-web的版本就由父级统一管理,不手写可避免和Spring Boot内部依赖打架;mybatis和mysql连接器不在父级管理范围里,必须显式声明。如果自己重新搭项目,版本优先去Maven中央仓库搜索当前可用的,别照抄五年前教程里的号码,版本冲突是Spring Boot启动失败最常见的一类原因,属于启动即翻车的典型。
3. 数据库设计到后端编码:先把预约业务跑通
3.1 四张核心表怎么建:会员、项目、预约、订单
美容院业务看起来杂,核心数据就四张表:会员表存客户基本信息和储值余额,项目表存服务名称、时长、价格,预约表记录顾客约了哪个项目、什么时间到店,订单表存每次确定的消费记录。员工排班、库存、积分这类都算扩展,先把四张核心表建出来,系统的主干就跑通了。
CREATE DATABASE IF NOT EXISTS beauty_saas DEFAULT CHARSET utf8mb4; USE beauty_saas; CREATE TABLE t_member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT '会员姓名', phone VARCHAR(20) NOT NULL UNIQUE COMMENT '手机号,登录账号', balance DECIMAL(10,2) DEFAULT 0.00 COMMENT '储值余额', level TINYINT DEFAULT 1 COMMENT '会员等级 1普通 2银卡 3金卡', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='会员表'; CREATE TABLE t_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT '项目名称', duration_minutes INT DEFAULT 60 COMMENT '单次耗时', price DECIMAL(10,2) NOT NULL COMMENT '单次价格', status TINYINT DEFAULT 1 COMMENT '1上架 0下架' ) ENGINE=InnoDB COMMENT='项目表'; CREATE TABLE t_appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL COMMENT '预约的会员', item_id BIGINT NOT NULL COMMENT '预约的项目', appoint_time DATETIME NOT NULL COMMENT '预约到店时间', status TINYINT DEFAULT 0 COMMENT '0待服务 1已服务 2已取消' ) ENGINE=InnoDB COMMENT='预约表'; CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, item_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL COMMENT '实收金额', pay_type TINYINT DEFAULT 0 COMMENT '0储值 1现金 2微信', order_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='订单表';几个细节值得细说。手机号加UNIQUE约束,因为美容院场景里顾客就靠手机号识别,登录、查单、绑卡全走这个字段。金额用DECIMAL而不用FLOAT,涉及钱的字段用浮点会在累计后出现0.30000000000000004这类精度问题,答辩时被问到会很难解释。预约表和订单表分开,预约可能被取消,订单是确定消费,混在一张表会让“取消率”“月度营收”这类统计逻辑全乱。状态字段用TINYINT而不是VARCHAR存中文,一是节省空间,二是避免前后端对“已服务”和“已完成”这类同义词扯皮,状态含义统一在后端常量类里定义。
3.2 后端增删改查:一个会员模块的完整示例
后端按三层写:Controller接收参数、Service写业务逻辑、Mapper访问数据库。下面是一个最简会员注册和按手机号查询的Controller:
@RestController @RequestMapping("/api/member") public class MemberController { @Resource private MemberService memberService; @PostMapping("/register") public Result register(@RequestBody MemberRegisterReq req) { if (!StringUtils.hasText(req.getPhone())) { return Result.error("手机号不能为空"); } Member member = memberService.register(req.getPhone(), req.getName()); return Result.ok(member, "注册成功"); } @GetMapping("/{phone}") public Result findByPhone(@PathVariable String phone) { MemberVO vo = memberService.findByPhone(phone); return Result.ok(vo); } }register接口接收JSON请求体,入口处先把空手机号这种基础校验挡住,再交给Service处理开卡逻辑。findByPhone用路径参数写法,小程序端拼接URL直接就能用。Result类是全局统一返回结构的工具类,在项目里先定义好,每个Controller都返回它,第二章节的约定才算真正落地。
Service层最关键的是查重和事务:
@Service public class MemberServiceImpl implements MemberService { @Resource private MemberMapper memberMapper; @Override @Transactional(rollbackFor = Exception.class) public Member register(String phone, String name) { if (memberMapper.countByPhone(phone) > 0) { throw new BizException("该手机号已注册"); } Member m = new Member(); m.setPhone(phone); m.setName(name); m.setBalance(BigDecimal.ZERO); m.setLevel(1); memberMapper.insert(m); return m; } }@Transactional(rollbackFor = Exception.class)的含义要讲清楚:方法内任何异常都会让数据库操作整体回滚。Spring默认只对运行时异常回滚,checked异常不回滚,这里显式声明Exception.class才能把自定义BizException也算进去。这是后面排查“余额没扣但订单生成了”这类问题的理论依据。
Mapper层用MyBatis注解写SQL,对新手最直观:
@Mapper public interface MemberMapper { @Select("SELECT COUNT(*) FROM t_member WHERE phone = #{phone}") int countByPhone(String phone); @Insert("INSERT INTO t_member(name, phone, balance, level) " + "VALUES(#{name}, #{phone}, #{balance}, #{level})") int insert(Member member); }这套增删改查骨架几乎可以复制到项目、预约、订单每个模块,区别只在于字段和SQL。实际做毕设时建议先把会员模块完整跑通,再照着加另外三个模块,一次只走通一条链路,比同时铺四个模块再逐个排查接口要快得多。用注解写SQL的优点是代码都在一个类里,不用来回翻XML映射文件。
3.3 数据源与连接池参数:别让配置拖垮联调
数据库连不上,多半不是网络问题而是连接串参数缺一个。Spring Boot的application.yml是黑匣子重灾区,同样的代码,换个环境就报时区错误、SSL握手失败、中文乱码,原因都藏在url里:
spring: datasource: url: jdbc:mysql://localhost:3306/beauty_saas?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000参数逐个说:serverTimezone=Asia/Shanghai解决数据库时间比本地早8小时的老问题;useSSL=false避免MySQL 8.0默认开启SSL协议导致的握手告警和连接变慢;characterEncoding=utf8防止中文乱码,加上useUnicode=true是JDBC标配写法。连接池用HikariCP,Spring Boot 2.x默认就是它,零额外依赖。如果手头工程用的是Druid,那要在pom.xml里显式加依赖,并在配置里写type指定DruidDataSource。
连接池参数不必调大。毕设项目的并发量很小,maximum-pool-size设10已经有余量,minimum-idle设2是让空闲时保留两条连接,避免频繁建连的延迟。connection-timeout=30000表示30秒内拿不到连接就报超时,真出现这个错,先查MySQL服务是否启动、账号密码是否正确,再回头看连接池配置。很多人遇到问题第一反应是重启,那是把环境问题掩盖成代码问题,答辩时追问两句就穿帮。
4. 小程序端搭建与联调:从页面到接口的全链路
4.1 页面结构与导航栏高度适配
小程序端通常三个主页面:首页展示项目列表和营业信息,预约页完成选项目、选时间、确认预约,我的页面展示会员信息和储值余额。app.json里的tabBar决定底部导航:
{ "pages": [ "pages/home/home", "pages/appointment/appointment", "pages/mine/mine" ], "tabBar": { "list": [ { "pagePath": "pages/home/home", "text": "首页" }, { "pagePath": "pages/appointment/appointment", "text": "预约" }, { "pagePath": "pages/mine/mine", "text": "我的" } ] } }页面数量不用多,三个tab足够覆盖美容院管理的全部顾客端操作。tabBar里icon字段这里省略,不影响运行,正式项目建议补上图标。这个工程量对应毕业设计节奏是合理的——前端三页、后端四组CRUD接口,主线之外再去补充细节。
导航栏高度是新手容易在页面布局上踩坑的地方。如果自定义导航栏,iPhone和安卓的状态栏高度不同,写死px必然错位。常见做法根据胶囊按钮位置动态计算:
function navBarHeight() { const menu = wx.getMenuButtonBoundingClientRect() const system = wx.getWindowInfo() const statusBarHeight = system.statusBarHeight return menu.top + (menu.height - statusBarHeight) * 2 }说明:胶囊按钮右侧垂直居中的位置是导航栏内容最安全的摆放位置,公式算出的高度在各类机型上都能适配。不要用statusBarHeight加固定数值这类魔法数字,真机一换设备就错位。真机调试时还要注意手机和电脑在同一局域网内,后端启动要监听0.0.0.0而不是localhost,不然请求会直接超时。
4.2 封装request:把登录态和错误处理收口到一处
如果每个页面直接调wx.request,token过期、接口超时、登录失效的逻辑会散落几十个文件。毕设系统不大,但这个封装值得做,答辩时可以讲成“统一请求层设计”。
// utils/request.js const BASE_URL = 'http://192.168.1.100:8080/api' const 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', 'token': wx.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 0) { resolve(res.data.data) } else if (res.data.code === 401) { wx.navigateTo({ url: '/pages/login/login' }) } else { wx.showToast({ title: res.data.msg, icon: 'none' }) reject(res.data) } }, fail: (err) => { wx.showToast({ title: '网络异常,请稍后重试', icon: 'none' }) reject(err) } }) }) } module.exports = { request }逻辑说明:BASE_URL在开发阶段填后端所在电脑的局域网IP,上线再换成HTTPS域名。header统一带token,后端靠它识别请求来自哪个会员,鉴权拦截器校验失败时返回code为401,小程序端统一跳登录页。code为0时resolve出业务数据,页面只关心成功分支;其他业务错误用toast提示后端返回的msg,不需要每个页面各写一套错误弹窗。网络层失败单独提示“网络异常”,和业务失败分开处理,排错时少绕弯。
用Promise配合async/await,页面调用时像写同步逻辑,不再嵌套地狱。拿到下载的源码时,第一件事检查request.js里有没有对res.data判空,有些旧封装直接访问res.data.code,后端一旦返回HTML错误页就报Cannot read property,这种问题在真机上一抓一个准。
4.3 登录态与缓存时间:token掉了比接口报错更伤脑筋
小程序的登录流程一般是:wx.login拿到临时code,交给后端换取openid,后端签发token返回,小程序端存入Storage。token的有效期就是缓存时间,过期后再次请求会收到401。
// 登录页面核心逻辑 const app = getApp() async function login() { const res = await new Promise((resolve) => { wx.login({ success: resolve, fail: () => resolve({ code: '' }) }) }) if (!res.code) { wx.showToast({ title: '微信登录失败', icon: 'none' }) return } const data = await request('/user/login', 'POST', { code: res.code }) wx.setStorageSync('token', data.token) wx.setStorageSync('memberInfo', data.member) wx.switchTab({ url: '/pages/mine/mine' }) }说明:wx.login不需要用户点授权,它只能拿到临时code,换openid必须由后端发起,openid这种身份标识不能暴露在小程序端。setStorageSync保存token,有效期建议设7天,符合美容院会员一两周到店一次的频率;有效期太短顾客频繁被踢回登录页,太长又削弱登录态校验的意义。后端签发token时把memberId放进有效载荷,预约接口就能直接读到当前用户,不用每次靠手机号反查。
预约流程的联调顺序有讲究:先用Postman把后端预约接口调通,再写小程序预约页。不少人先写页面再调接口,字段名对不上再改页面,纯属浪费时间。字段一致性依赖第2章的统一返回约定——后端返回memberId,小程序端也全用memberId,不建议转成member_id,联调期少一层转换就少一类问题。预约成功后跳转“我的”页面能看到预约记录,这条链路就是整个项目的验收主线。
5. 避坑排查与验证:真机上跑通才算数
5.1 四个让人半夜改代码的坑
真机上所有请求失败,但开发者工具正常。原因:开发者工具里的“不校验合法域名”开关只影响工具内调试,手机预览时请求会被拦截,报“不在以下合法域名列表”。解决:开发阶段用真机调试并暂时勾选跳过域名校验,正式上线必须把后端部署到已备案的HTTPS域名下。
数据库时间比本地早8小时。原因:MySQL默认时区是UTC,JDBC连接串没带serverTimezone参数。解决:url末尾加serverTimezone=Asia/Shanghai,重启后端即可。
上传的图片前端打不开。原因:Spring Boot默认不把磁盘路径映射成URL,文件写进了D:/upload,页面却访问http://ip:8080/upload/xx。解决:写一个WebMvcConfigurer把本地目录映射到/upload/**,或者把上传目录直接设到项目static下。
页面显示已下单但会员余额没变。原因:扣余额和生成订单两个数据库操作不在同一个事务里,订单先提交,扣余额抛异常没回滚。解决:把两个操作合到同一个Service方法,加@Transactional(rollbackFor=Exception.class),确保同生共死。
5.2 答辩前的功能验证清单
拿一台真机按顾客路径完整走一遍:首次进入小程序能注册或登录→浏览项目列表→预约一个项目→预约列表能看到记录→会员储值余额正确扣减→订单表出现对应订单。每走一步去数据库查一下对应表,确认数据真的落了库,这一条能提前暴露“页面提示成功但库里没数据”的隐藏bug。后端接口用Postman批量跑一遍,重点看code非0时的错误文案是否对用户友好,比如余额不足提示而不是一串异常堆栈。
5.3 怎么把它改成不撞车的毕设
直接拿原题交,同班同学可能撞。低成本加功能的方向有三个:预约从只选日期改成选时间点并关联技师,给员工加一张排班表;会员等级从固定值改成按消费积分自动升级;首页加一个近7日营收的小柱状图。任何一个改动都够在答辩时讲五分钟。技术栈也可以微调,把MyBatis注解换成MyBatis-Plus的LambdaQueryWrapper,减少样板代码,但不要在简单系统里硬套微服务分散精力。
我自己的习惯是改完系统一定同步更新说明文档,答辩老师是照着文档点页面的,文档写的路径和页面实际行为对不上,比系统有bug更扣分。折腾过几轮毕设,最值钱的一条心得是先跑通再优化,希望帮到你。
本文还有配套的精品资源,点击获取