1. 项目概述与核心需求解析
1.1 这个项目到底在解决什么问题
做毕设选“微信小程序健身房管理系统”这个方向的人,这两年我见了不少。为什么这个选题受欢迎?因为它两头都占:前端是当下最主流的小程序载体,后端又足够承载一个完整的管理系统应有的业务逻辑。健身房这个场景天然自带会员、课程、教练、订单、统计几大模块,每一个单独拎出来都能写出不少业务代码,但也都不至于复杂到做不完。对毕设来说,这是性价比很高的选题。
但很多人一开始会把“健身房管理系统”理解成简单的“会员信息增删改查”。这就太亏了。你想想,如果只是一个CRUD系统,评委会怎么看?现在毕设的评审标准早就不是“能跑就行”了,而是能不能体现你对业务的理解、对工程化结构的掌握、对异常情况的处理能力。一篇合格的健身房小程序毕设,首先应该是一个完整的业务闭环:用户通过小程序注册登录、浏览课程、在线预约、到店核销,管理员在后台管理课程排期、处理会员卡、查看运营数据。这几条链路串起来,才是一个真正有说服力的毕设。
这个项目适合谁来参考?一类是正在选题的大四学生,你可以通过这份拆解判断工作量是否合适;另一类是已经有基础Java或前端基础、想通过一个完整项目巩固开发能力的开发者。我下面的内容会从技术选型、功能拆解、核心实现、踩坑实录四个维度展开,尽量把这些年在类似项目上沉淀的经验都讲清楚。
1.2 需求拆解:从用户故事反推功能边界
先别急着敲代码。在动手之前,我强烈建议你把项目按用户角色拆成三个视角,然后分别列出“他们打开这个系统最想做什么”。
- 普通会员:注册登录、查看健身房介绍和课程排期、预约团课或私教课、查看我的预约记录、取消预约、办理或续费会员卡、查看个人运动数据。
- 教练角色:查看被预约的课程列表、确认或取消课程、提交课程反馈(比如学员到课情况)。
- 管理员:维护教练和课程信息、发布或调整排课、处理会员卡办理请求、查看预约统计和营收数据、管理公告通知。
三个角色对应的终端也不同。会员和教练用小程序的C端入口,管理员则在Web端后台操作。这里注意,很多毕设会把管理员功能也塞进小程序里,我不是特别推荐。一方面,小程序端面向的是高频轻量操作,后台管理涉及大量表格和表单操作,在手机小屏上体验很差;另一方面,后台独立成一个Web端能显著增加你的系统架构内容,写论文时能多出一个“多端协同”的章节,工作量也更饱满。我做的版本就是“小程序C端 + Vue管理后台 + 后端服务”,三方通过统一的RESTful API通信。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计
2.1 前端方案:为什么要走原生小程序而不是Uniapp
微信小程序前端有两条主流路线:原生开发,或者用Uniapp/Taro跨端框架。毕设场景下,我建议优先选原生。
原因有三点。第一,原生框架不存在编译层,遇到问题时社区答案最多,你Google任何一个报错基本都能找到对应的解决方案;第二,原生小程序的WXML和WXSS对微信生态的适配最直接,像wx.login、wx.request、订阅消息这些API,在原生环境里踩坑最少;第三,评审阶段老师如果抽查代码,原生代码的可读性比编译产物好得多。
有的同学担心原生开发效率低,觉得Uniapp写起来更快。但你要考虑到,Uniapp的坑在于“编译后表现与预期不一致”,尤其在涉及特定API和组件时,调试成本会翻倍。毕设有明确的时间节点,最怕的就是期限快到还在跟框架特性搏斗。我的经验是:老老实实原生写,页面复杂度其实不高,几十个页面文件按规范组织好,完全可控。
2.2 后端选型:SSM是稳妥牌,Spring Boot是加分项
后端我建议直接选Spring Boot,然后搭配MyBatis-Plus。为什么不是传统的SSM(Spring + SpringMVC + MyBatis)?因为现在Spring Boot已经是JavaWeb领域的事实标准,它的自动装配、嵌入式容器、起步依赖这三大特性,能帮你砍掉大量XML配置,把精力集中在业务代码上。你写论文时关于“为什么选择Spring Boot”这段也好编:自动配置简化、生态成熟、部署方便。
MyBatis-Plus则是MyBatis的增强版,它的BaseMapper直接内置了单表CRUD方法,意味着你连基础的SQL都不用写。在用户表、预约表、课程表这些基础实体上,直接继承接口就能操作数据库。很多同学抵触用这类工具,觉得它是“偷懒”,但作为工程实践,MyBatis-Plus不仅不违背规范,反而能在真实工作中提高开发效率。你只要在复杂查询时手写SQL,并不影响项目含金量。
数据库用MySQL 5.7或8.0,设计上遵守三范式,但适当做冗余。比如预约表里冗余一个课程名称字段,查询时就能少一次连表操作,这在业务上是非常常见的优化手段。
2.3 整体架构:请求是怎么跑通的
我从一次真实请求出发,帮你把全链路捋一遍。
比如说,用户在小程序首页点击“查看课程列表”。小程序端通过wx.request发起HTTP请求到你的后端接口,后端Controller接收参数后调用Service层,Service层通过MyBatis-Plus操作MySQL,拿到结果后封装成统一的JSON结构返回给小程序端。小程序端拿到数据后用setData更新页面视图。
整个过程中,有两个关键设计值得你写进论文里:统一返回封装和全局异常处理。
- 统一返回封装:定义
Result<T>类,包含code、message、data三个字段。所有接口无论成功失败都返回这个结构。小程序端判断code是否为200,是就取数据,否则弹错误提示。这个设计能让你后续排查问题非常方便,因为你不用猜接口返回的到底是什么格式。 - 全局异常处理:在Spring Boot里用
@RestControllerAdvice注解捕获所有业务异常,统一转成上面说的错误格式。这样Controller里就不需要到处写try-catch了,代码会干净很多。
我见过不少毕设代码,每个接口的返回格式都不一样,有的成功返回data,失败返回errorMsg,小程序端要写满if-else去兼容。这种代码写起来痛苦,答辩时被老师一问也容易露怯。统一封装和全局异常处理,这两件事做在前面,能给你省下大量麻烦。
3. 核心功能模块的详细设计
3.1 登录鉴权:这次不整复杂的JWT
小程序登录鉴权方案有很多种,JWT是其中相对流行的一种。但说句实话,毕设场景用Session登录就够了,不用为了“听起来高级”硬上JWT。
微信小程序登录的官方流程是这样的:
- 前端调用
wx.login(),拿到一个临时code。 - 前端把
code传给后端。 - 后端拿着
code,加上小程序的appid和secret,请求微信的接口jscode2session。 - 微信返回
openid(用户唯一标识)和session_key。 - 后端用
openid查数据库,如果该用户第一次登录就创建用户记录,然后生成一个会话并返回token给前端。
这里有个很重要的点:同一个微信用户在同一个小程序下的openid是唯一的,所以你根本不用要求用户输入用户名密码,也不需要自己去实现一套注册登录机制。用户第一次登录时绑定手机号,后续再进入时直接静默登录,体验是非常顺畅的。
我见过一些同学在这个环节卡住,情况通常是:wx.login()拿到了code,后端拿着code请求微信接口时报错40029 invalid code。原因一般有两个:一是appid和secret不匹配,二是code不是一次性使用,已经被消耗掉了。排查时先确认你在微信公众平台拿到的AppSecret和你后端配置的是同一个,再确认是不是同一个code被请求了两次。
登录态这块我建议用最朴素的方式:后端生成一个UUID字符串作为token,存到Redis里,设置过期时间为7天。前端把token存到小程序的storage里,每次wx.request时通过请求头携带。后端写一个拦截器,除了登录接口和公开接口外,所有请求都验证token有效性。这套流程虽然简单,但完整覆盖了“认证”和“授权”两个层面,写到论文里也很完整。
3.2 课程预约:难点是并发控制和状态机
课程预约是健身房系统的核心,也是最容易出技术亮点的地方。我先讲业务规则,再讲实现。
业务规则大概是这样:管理员在后台维护课程表,包括课程名称、教练、上课时间、名额上限、预约截止时间。会员在小程序端看到课程列表,可以预约未满员的课程。预约成功后,该课程已预约人数加1。会员也可以在规定时间内取消预约,取消后名额释放。
这个流程看似简单,但当你考虑多人同时抢最后一个名额时,就会遇到并发问题。如果没有做任何处理,两个用户同时提交预约请求,系统可能两个都通过,导致超卖。解决方式我在实操部分会详细展开,这里先告诉你思路:用数据库的唯一约束兜底,或在事务里对课程记录加行锁。
另外一个值得写进论文的设计是“预约状态机”。一个预约记录应当处于以下几个状态:已预约、已完成、已取消、已爽约。状态转换规则是:已预约可以转为已完成(后台确认到课)、已取消(用户主动在截止时间前取消)、已爽约(到了上课时间用户未到且未取消)。用状态机的方式设计,代码逻辑清晰得多,也容易扩展新状态。
这里有一个细节:课程表里要有一个appointment_deadline字段,即预约截止时间。判断逻辑是“当前时间小于截止时间才允许取消”。这个字段可以是课程开始前30分钟,也可以是开课前2小时,具体取决于健身房规则。我当时做的是默认开课前30分钟。
3.3 会员卡管理:计时与权限校验
会员卡模块是健身房系统区别于一般预约系统的差异化功能。健身房的核心商业模式是把“次卡”和“时长卡”打包卖给用户,所以你的系统必须能处理以下两类会员卡:
- 次卡:比如“30次健身卡”,每来一次扣一次,次数用完作废。
- 时长卡:比如“季卡”“年卡”,从激活之日起到截止日期内不限次数使用。
在设计数据表时,会员卡表要包含以下字段:卡类型(次卡/时长卡)、总次数或总天数、剩余次数、生效时间和到期时间。为了简化,通常还有一张“用户-会员卡关系表”,记录哪个人买了哪张卡、订单状态是已支付还是待支付。
会员的每次预约或到店都需要校验卡的有效性。这里贴一段我用Java写的校验逻辑思路:
java复制public boolean checkMembershipValid(MembershipCard card) {
if (card == null) {
return false;
}
if ("COUNT".equals(card.getType())) {
return card.getRemainingTimes() > 0;
}
if ("DURATION".equals(card.getType())) {
LocalDate today = LocalDate.now();
return !today.isBefore(card.getStartDate()) && !today.isAfter(card.getEndDate());
}
return false;
}
这个环节还有一个很多毕设忽略的细节:购买会员卡时需要在后台生成订单记录,即使不真正对接微信支付,也要预留一个“待支付”“已支付”状态的流转过程。做毕设时可以用模拟支付接口代替真实支付,也就是用户点击支付时直接弹一个“模拟支付成功”,后台把订单状态从待支付改成已支付。如果你在论文里能说明清楚“这里用模拟支付代替,后续可对接微信支付接口”,就完全站得住脚。
3.4 数据统计:给管理员的经营视角
健身房管理者不可能天天盯着数据库看,他们需要一个可视化页面看到:今日预约人数是多少、本周课程饱和度如何、本月新办会员卡多少张、最受欢迎的课程是哪几节。
这些统计功能在毕设里属于增色项,但很多同学做的时候容易犯一个错误:在前端循环遍历所有记录再自己加总。这个做法数据量大了以后会非常卡,而且完全没有必要。正确答案是:在数据库查询时就用聚合函数。
比如统计本周每天的预约量:
sql复制SELECT DATE(create_time) AS day, COUNT(*) AS cnt
FROM appointment
WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY DATE(create_time)
ORDER BY day;
在后端再按照日期把数据整理成前端图表需要的JSON格式。前端用ECharts或者uCharts就能画出漂亮的折线图。这些可视化图表贴到论文里,效果比你写一千字描述功能都好。
4. 数据库设计与关键表结构
4.1 数据表整体规划
数据库设计是你在写论文时必须认真对待的一章。我建议至少设计以下这些表:用户表(member)、管理员表(admin)、课程表(course)、教练表(coach)、排课表(schedule)、预约表(appointment)、会员卡类型表(membership_type)、用户会员卡表(user_membership)、订单表(order)、公告表(announcement)。
下面我挑几张核心表讲解字段设计,你可以直接按这个思路去扩展。
4.2 用户表与课程表
用户表是最基本的表,字段包括:id、openid(微信唯一标识)、nickname、avatar_url、phone、gender、create_time。注意,openid字段必须加唯一索引,因为它是关联微信身份的唯一凭证。
课程表字段包括:id、course_name、description、course_type(团课/私教)、coach_id(关联教练表)、duration_minutes(课程时长)、max_capacity(名额上限)。课程表不存具体的上课时间,因为同一门课每周可能有多节,具体节次放在排课表里。
排课表(schedule)字段:id、course_id、class_date(上课日期)、start_time、end_time、booked_count(已预约人数)、status(可预约/已满员/已取消)。booked_count这个字段在并发场景下是重点,我会在实操部分专门讲。
4.3 预约表与订单表
预约表字段:id、user_id、schedule_id、status(已预约/已完成/已取消/已爽约)、create_time、cancel_time。这个表要加两个索引:user_id和schedule_id,因为查询个人预约记录和某节课的预约列表是最高频的操作。
订单表字段:id、order_no(订单编号)、user_id、membership_type_id、amount(金额)、status(待支付/已支付/已取消)、pay_time、create_time。order_no要唯一,一般用时间戳加上随机数生成。
表设计的总体原则就是“高内聚低冗余”,但这不意味着绝不冗余。比如预约表里我建议加一个course_name字段,把课程名直接存进去,这样查预约记录时不需要join课程表就能展示课程名。数据库三范式是理论最优,但在实际业务中,适当冗余能减少联表次数,提升查询效率,这本身就是工程上的取舍。
5. 实操过程与核心代码实现
5.1 前端:小程序端登录与请求封装
小程序端首先要解决的就是统一的请求封装。我在项目里创建了一个utils/request.js文件,核心代码如下:
javascript复制const BASE_URL = 'http://localhost:8080/api';
function request(url, method = 'GET', data = {}) {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token');
wx.request({
url: BASE_URL + url,
method: method,
data: data,
header: {
'Content-Type': 'application/json',
'token': token || ''
},
success: (res) => {
const { code, message, data } = res.data;
if (code === 200) {
resolve(data);
} else if (code === 401) {
wx.navigateTo({ url: '/pages/login/login' });
reject(new Error(message));
} else {
wx.showToast({ title: message, icon: 'none' });
reject(new Error(message));
}
},
fail: (err) => {
wx.showToast({ title: '网络错误', icon: 'none' });
reject(err);
}
});
});
}
module.exports = { request, BASE_URL };
这样做的好处是,你所有页面里都不需要写wx.request原生的回调逻辑,直接调request('/course/list', 'GET').then(res => {...})就行。401状态码代表token过期,会自动跳到登录页。这个封装是我在多个项目里反复验证过的结构,代码量少但非常顺手。
登录页面核心代码如下:
javascript复制handleLogin() {
wx.login({
success: async (res) => {
const code = res.code;
const loginRes = await request('/auth/login', 'POST', { code: code });
wx.setStorageSync('token', loginRes.token);
wx.setStorageSync('userInfo', loginRes.userInfo);
wx.switchTab({ url: '/pages/index/index' });
}
});
}
这里有个体验细节:微信官方已经不再推荐强制用户点击“授权登录”弹窗,因为wx.getUserProfile在基础库2.27.1版本之后调整了调用策略,现在获取手机号也需要单独授权。所以我的策略是:新用户首次登录后先以默认昵称进入系统,然后弹出一个“完善个人资料”的引导页,用户自愿填写昵称和头像。这样既规避了授权策略变动的问题,也符合微信审慎对待用户隐私的态度。
5.2 登录态校验:后端拦截器实现
后端要做的对应工作是登录接口和token拦截器。登录接口接收前端传来的code,调用微信jscode2session接口换openid,查询并创建用户,生成token返回。
java复制@PostMapping("/login")
public Result<String> login(@RequestBody LoginRequest request) {
// 1. 通过code获取openid
String openid = wechatService.getOpenid(request.getCode());
// 2. 查询用户是否存在,不存在则创建
User user = userMapper.selectByOpenid(openid);
if (user == null) {
user = new User();
user.setOpenid(openid);
userMapper.insert(user);
}
// 3. 生成token并存入redis,有效期7天
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set("token:" + token, JSON.toJSONString(user), 7, TimeUnit.DAYS);
return Result.success(token);
}
拦截器的作用是校验每个请求的token,并从Redis里取出用户信息放到ThreadLocal中,这样后续的Controller和Service里随时可以拿到当前用户,不需要每次请求都解析token。
java复制@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
throw new BusinessException(401, "未登录");
}
String userJson = redisTemplate.opsForValue().get("token:" + token);
if (StringUtils.isEmpty(userJson)) {
throw new BusinessException(401, "登录已过期");
}
// 存入ThreadLocal,供后续使用
UserContext.set(JSON.parseObject(userJson, User.class));
return true;
}
5.3 并发预约:一行代码解决超卖
课程预约是系统里最需要谨慎处理的地方。假设一门课的名额上限是10人,当前已有9人预约,第10个和第11个用户几乎同时提交预约请求,如果不加控制,两个请求都通过了事务检查,booked_count就会变成11,超出了名额上限。
解决超卖最保险的方案有两种。
第一种是更新时做条件校验,用乐观锁思路:
java复制int rows = scheduleMapper.updateBookedCount(scheduleId, maxCapacity);
// UPDATE schedule SET booked_count = booked_count + 1
// WHERE id = #{scheduleId} AND booked_count < #{maxCapacity}
if (rows == 0) {
throw new BusinessException("课程名额已满");
}
这种方式利用的是UPDATE语句受影响的行数。当booked_count小于maxCapacity时才能更新成功,否则受影响行数为0,代表名额满了。因为是数据库原子的更新操作,天然并发安全,性能也很好。
第二种方案是悲观锁,在事务里用SELECT ... FOR UPDATE锁定课程记录,等事务结束后其他请求才能读取。优点是不会出现误判,缺点是并发量很大时会阻塞。在健身房场景下,并发预约量撑死也就几百个人同时抢课,这两种方法都足够用。我实际项目里用的是第一种,代码简洁且不用考虑事务隔离级别问题。
这里要特别注意一个坑:如果用了@Transactional,你要确保事务确实生效。MyBatis-Plus + Spring Boot环境下,要检查你的Service方法是不是被代理调用,也就是不能同类内调用。如果你在同一个类里从methodA()调methodB(),@Transactional可能失效,这是很多新手踩过的最隐蔽的坑。
5.4 取消预约与名额释放
取消预约的逻辑看起来简单,修改状态就行,但有几个边界条件必须判断清楚:
- 当前时间是否晚于截止时间?如果晚了,提示“已过截止时间,无法取消”。
- 当前用户是否是这条预约记录的本人?必须校验,防止越权操作。
- 取消成功之后,要释放名额,即
booked_count减1。
一个完整的事务应该同时更新预约表状态和排课表已预约人数,这一步必须在一个@Transactional方法里完成。如果只改预约表忘了改排课表,就会出现“预约记录显示已取消,但课程名额仍然被占用”的脏数据问题。我当时排查这类问题耗费了不少时间,这里写出来帮你避坑。
6. 部署与联调:从本地到真机的完整过程
6.1 本地开发环境的准备工作
后端是Spring Boot项目,你本地需要准备的软件是:JDK 1.8(或11)、Maven 3.6+、MySQL 5.7+、Redis(用于存放token)。建议用IDEA开发,社区版就够用。
小程序端用微信开发者工具打开项目目录即可,记得在project.config.json里把appid改成你自己的。注意,如果只是在开发者工具里调试,可以使用测试号,不需要注册小程序账号;但如果你想真机预览,就必须要有一个已注册的小程序AppID。
数据库初始化:在MySQL里执行你写好的init.sql,里面包含建库建表语句和初始数据。我建议初始数据多插入几条课程记录、教练记录,这样小程序端界面上不至于空空荡荡,演示效果也更好。
6.2 小程序端如何连上本机后端
这是让很多同学头疼的问题:开发者工具里使用本机回环地址访问后端,真机预览时却连不上。原因在于,真机上访问localhost访问的是手机自己,而不是你的电脑。
解决办法:在小程序开发者工具中勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”,然后在真机调试时,把请求地址从http://localhost:8080改成你电脑在局域网中的IP地址,比如http://192.168.31.100:8080。
具体操作是:
- 在微信开发者工具的“详情-本地设置”里勾选“不校验合法域名”。
- 后端启动时不要只监听
localhost,要监听0.0.0.0。Spring Boot默认是监听所有网卡的,一般没问题,但如果你自定义过配置要检查一下。 - 手机和电脑连同一个WiFi,把请求地址改成电脑的局域网IP。
- 真机预览时,在开发者工具右上角点击“预览”,生成二维码,手机扫码调试。
这里有一个非常常见的报错:真机上请求直接失败,显示net::ERR_CONNECTION_RESET。造成这个报错最常见的原因是后端服务没有真正启动,或者你手机访问电脑IP的8080端口时被防火墙拦截了。Windows上你需要到“防火墙-高级设置-入站规则”里放行Java或8080端口。
我们开发时踩过几次坑后总结了一个更省事的方案:后端部署到云服务器,小程序直接请求服务器的公网接口。这样不仅免去防火墙问题,而且整个流程和真实上线部署完全一致。唯一要注意的是,小程序的正式环境要求后端域名必须备案且为HTTPS,不过这是上线前才需要处理的事,毕设演示阶段用IP+端口就够了。
6.3 后端打包与运行
后端打包成Jar包运行是必须掌握的操作。在项目根目录执行:
bash复制mvn clean package -DskipTests
打包完成后,在target目录下会生成一个jar文件。上传到服务器后运行:
bash复制java -jar gym-system-1.0.0.jar --spring.profiles.active=prod
线上和本地环境的数据库连接、Redis地址、日志级别都可能不同,所以建议在src/main/resources下创建application-dev.yml和application-prod.yml两套配置。通过--spring.profiles.active参数切换,这样代码里不用改动任何地方,只改配置就能适配多环境。这个设计也会成为你论文里的一个专业亮点。
6.4 小程序审核与发布前检查
如果你的系统不只是用来答辩演示,还需要上传体验版,甚至发布到线上的话,有一些细节需要提前注意。
- 小程序后台需要在“开发-开发设置”里配置服务器域名。注意,
request合法域名要求HTTPS,而且不能带端口号。 - 小程序类目选择:健身房相关的服务类目建议选“生活服务-体育”或“工具-信息查询”,选错类目会导致部分功能审核时被拒。
- 如果涉及会员卡购买,即使只是模拟支付,在审核时也可能被要求补充支付相关资质。如果不想折腾,就保持“模拟支付”并把它定义为演示功能,上线版可以去掉这个入口。有同学在答辩时演示了购买会员卡再模拟支付成功,评委也认可这个逻辑,没有遇到阻碍。
7. 常见问题与排查技巧实录
7.1 微信登录报错:获取登录后的微信用户失败
这个报错很常见,尤其在旧版本代码里调用了wx.getUserInfo()而不是wx.getUserProfile()时。2021年之后微信对用户信息接口做了收紧,wx.getUserInfo不再返回真实的头像昵称,会返回默认的灰色头像和“微信用户”昵称,在小程序基础库较高版本中甚至直接报错。我在项目中处理的方法是使用wx.getUserProfile,并且将用户主动点击的触发方式绑定在按钮事件上,不能直接放在onLoad里调用。
如果后端调用jscode2session时报40029 invalid code,排查思路如下:
- 先确认前端的
wx.login()执行成功,拿到的是新的code。 - 再确认后端配的
appid和secret与你的小程序一一对应。特别注意:公众平台里同一个邮箱下可能有多个应用,千万别拿错AppSecret。 - 检查
code是否被重复使用。code有效期只有5分钟,且只能使用一次,调试时如果你刷新页面太多次,会用掉多个有效code,所以不要在一个流程里多次调用登录。
7.2 真机测试连接不上后端接口
前面已提到ERR_CONNECTION_RESET,我再补充一些排查细节:
- 先在电脑浏览器里访问接口地址,确认后端确实能响应。
- 在电脑上打开命令行执行
ipconfig,确认你电脑的局域网IP地址。 - 在手机上用浏览器访问相同地址,如果浏览器也打不开,那就是防火墙或网络问题;如果浏览器能打开但小程序不行,问题可能出在小程序配置上。
- 检查是不是用了
http而小程序要求https。在开发调试阶段可以通过详情设置里关掉合法域名校验来绕过,但真实上线必须要HTTPS。
7.3 预约功能超卖或查不到记录
如果你用UPDATE ... WHERE ...条件更新的方案,不会有超卖问题。但如果线上还是出现超卖,检查你是否真的把booked_count < maxCapacity这个条件写在SQL里了,而不是只写了booked_count = booked_count + 1。这是多写一个条件的事,但很多人会忘记。
还有一种情况是:取消预约后,名额没有释放。排查时先手动改数据库观察appointment表和schedule表的数据是否一致。如果是,就去看Service层是否用了@Transactional,以及事务是否真的生效了。
7.4 小程序端页面数据不刷新
小程序最让人抓狂的问题之一就是setData后页面不更新。最常见的原因是在onLoad里发送了异步请求,但回调里没有用箭头函数,导致this指向错误。例如:
javascript复制// 错误写法
wx.request({
success: function(res) {
this.setData({ list: res.data }); // this不是Page实例
}
});
// 正确写法
wx.request({
success: (res) => {
this.setData({ list: res.data });
}
});
另一个原因是直接在本地修改this.data.list然后调用this.setData({ list: this.data.list }),此时页面检测不到变化。解决方案是每次setData都传入新数组,或者用展开运算符生成新引用:
javascript复制const newList = [...this.data.list, ...newItems];
this.setData({ list: newList });
7.5 常见问题速查表
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
登录接口报40029 |
appid/secret不匹配或code被重复使用 | 核对配置,保证code只调用一次 |
真机请求失败ERR_CONNECTION_RESET |
防火墙拦截或后端未监听对应端口 | 放行端口,确认IP可达 |
| 预约超卖 | 缺少条件更新或事务失效 | 在UPDATE语句中加booked_count < max_capacity |
| 取消预约后名额未释放 | 事务未生效或漏更新排课表 | 检查@Transactional代理,确保两个表在同一事务 |
页面setData不刷新 |
this指向错误或旧数组引用 | 改用箭头函数,setData传新引用 |
| 上传代码提示无权限 | 不是项目成员或appid错误 | 在小程序后台添加项目成员 |
| 获取不到用户头像昵称 | 调用了wx.getUserInfo |
改用wx.getUserProfile,且绑在用户点击事件上 |
8. 论文写作与答辩准备的建议
8.1 论文结构怎么排版
评审老师看毕设论文,最关注的是三个部分:选题意义、系统设计、系统实现。你的论文结构可以这样组织:
第一章绪论,写课题背景和意义、国内外研究现状。这里的“研究现状”建议去查几篇实际文献,引用时要标注作者和年份,千万不要大段贴百度百科内容。
第二章相关技术介绍,分别介绍微信小程序、Spring Boot、MySQL、Redis,每项写2-3页足够。这里注意不要过于泛泛而谈,每项技术都要结合自己的项目说明为什么用它。
第三章系统需求分析,画用例图和功能模块图,分角色描述功能需求。需求分析部分要特别详细,因为这是体现你对业务理解的关键章节。教师角色、会员角色、管理员角色的用例要分开画,每个用例配上文字描述。
第四章系统设计,包括总体架构设计、功能模块设计、数据库设计。数据库设计要贴出每张表的建表SQL和ER图,并解释字段含义。
第五章系统实现,按模块贴关键代码并配截图。每贴一段代码都要说明这段代码在系统里承担什么功能,解决了什么问题。截图要保证清晰,页面要处于有数据的正常状态。
第六章系统测试,写测试用例和测试结果,包含功能测试和性能测试。性能测试可以用JMeter或Postman的Runner功能,模拟多个用户同时预约,把你的并发控制方案验证一下,这个测试结果和截图直接放进论文里,非常加分。
8.2 答辩时会被问到的问题
答辩时老师常问的几个问题,我提前帮你整理一下:
- “为什么选择微信小程序而不是原生App?”答:小程序无需下载安装、开发成本低、微信生态内流量获取容易。同时前端内容可复用,后端接口也可以被其他端调用。
- “你的系统安全性如何?”答:使用token会话管理,拦截器统一鉴权,Redis存储会话,密码不存储明文,最关键的业务表做了唯一索引和条件更新防超卖。
- “遇到的最大的难点是什么?”答:并发预约导致的名额超卖问题,通过条件更新和数据库原子操作解决;或者真机调试时的网络配置问题。
- “系统如何扩展为生产可用?”答:对接真实微信支付、使用HTTPS域名、增加日志监控、使用Docker容器化部署。
这些问题只要你有实操经验,就完全不慌。最怕的是让同学代写或者纯粹在GitHub上拉下来改个名字,老师随便问一句代码细节就露馅了。所以我一直强调:这个项目不应该只拿来做“完成毕设”的工具,更重要的是你从开发过程中理解一个完整系统是怎么从零到一搭建起来的。
8.3 源码文档的组织方式
你最终提交的成果里,源码和文档的组织清晰度也占分。建议目录结构如下:
code复制gym-wechat-miniapp # 小程序前端
gym-server # 后端Spring Boot项目
gym-admin-web # 管理端前端(Vue项目)
docs/ # 文档目录
├── 需求分析说明书.docx
├── 系统设计说明书.docx
├── 数据库设计说明书.docx
├── 答辩PPT.pptx
└── init.sql
init.sql要放在最外层或者docs目录里,方便老师直接导入数据库运行。README文件里写清楚项目启动步骤、环境要求、默认账号密码,这些细节能体现你的工程素养。
9. 从毕设到真实项目的扩展方向
如果你答辩完还有精力和兴趣,有几个方向可以让这个项目从“课程作业”升级为“可商用系统”。
第一个方向是接入真实微信支付。微信支付需要商户号,学生不好申请,但小程序支付的服务端签名、回调验签、退款逻辑,你都一清二楚了。我在项目里把支付接口做成一个抽象层,客户端传下单参数,服务端统一处理,后续只需替换具体的支付实现类即可。
第二个方向是增加消息推送。课程被预约、上课提醒、会员卡到期提醒,这些都是可以推送订阅消息的场景。微信小程序的订阅消息是一次性的,需要用户主动授权,设计时要引导用户在预约时同时勾选“上课提醒”。小程序端调用wx.requestSubscribeMessage授权,后端通过subscribeMessage.send接口推送,这个功能做完整个系统就更有“活”的感觉。
第三个方向是数据可视化大屏。在管理后台里增加一个页面,展示课程预约热力图、用户活跃时段、教练受欢迎度排名,用ECharts实现。这个功能在答辩演示时视觉效果也很好。
第四个方向是使用Docker部署。把后端、MySQL、Redis分别容器化,用docker-compose一键启动,这样环境迁移和部署都会非常方便。虽然这些内容毕设不一定要求,但是对于你后续的职业生涯来说,理解容器化部署绝对是一项重要的技能储备。
你在做这些扩展时,会越来越清晰地感知到一个完整业务系统演进的过程:一开始只是为了跑通流程,后来关注效率、安全、体验,最后关注部署和运营。这种思维方式的转变,比你单纯提交一份毕设重要得多。
我这里所有的经验,都是基于自己实际做过的项目、走过的弯路沉淀下来的。做这个健身房管理系统的过程,既是在完成一项学术任务,也是对自己工程能力的一次全面检验。如果你正打算做或者正在做这个课题,希望这篇文章能帮你少踩一些坑,顺利走完整个流程。
