☰
SpringBoot+微信小程序:小学生托管管理系统实战全流程
2026/9/29 18:07:25 网站建设 项目流程

又到了一年两度的毕业设计选题季,托管管理系统这个题目每年都有一大批人做,但真正能把前后端串起来、把业务流程讲清楚的并不多。很多同学卡在最基础的地方:微信小程序怎么和后端通信?登录状态怎么维持?接送记录和签到记录怎么设计才不会乱?

这篇文章就围绕一个基于SpringBoot和微信小程序的小学生托管管理系统,把从业务拆解、数据库设计、核心接口实现,到小程序端联调、部署运行的完整链路捋一遍。内容偏实战,适合正在做类似课题、或者想快速入门SpringBoot全栈开发的同学参考。我会把当时设计时踩过的坑和后来复盘觉得最该注意的点也都写出来。

1. 托管业务拆解:先理清需求,再谈技术选型

1.1 托管班的日常运转到底有哪些事

做系统之前,我专门跑了两家托管机构,把他们的日常流程捋了一遍。所谓托管,其实就是放学后把孩子接到机构里,看着写作业、管顿饭、等家长来接。这里面有四个核心角色:家长、学生、老师、机构管理员。日常动作看起来简单,但拆成系统需求后其实不少:

  • 早晨或放学后学生到达托管班,需要记录入托时间;
  • 学生离开时,需要记录离托时间,并且要确认是哪个家长接走的;
  • 家长需要实时看到孩子到没到、走没走、作业完成状态;
  • 每月收费、缴费记录、欠费提醒;
  • 孩子生病或有事不能来,家长需要请假;
  • 机构发通知,比如放假安排、活动通知;
  • 管理员要能按班级、按时间查看考勤统计。

这些需求单独拎出来每一个都不难,但要在同一个系统里串起来,并且保证数据准确、流程可追溯,就需要在设计和实现上下一番功夫。

1.2 技术选型:SpringBoot与微信小程序为什么是绝配

后端选SpringBoot,理由很实在:生态成熟、资料多、答辩好讲。SpringBoot的自动配置机制让项目初期搭建成本极低,配合MyBatis-Plus,单表CRUD基本不用写SQL,开发效率很高。Java语言的强类型特性也让多人协作时更容易保持代码稳定。

前端选微信小程序,核心原因是家长端的使用门槛为零。托管班的家长群体里,很多是爷爷奶奶辈,让他们下载一个独立App根本不现实,但在微信里打开小程序、点一下就能看到孩子状态,几乎不需要学习成本。从技术角度说,小程序提供了完整的登录体系、模板消息订阅能力,而且开发调试工具很成熟,比H5在微信里的体验更可控。

这里我也对比过另一条路线:只做一个web管理后台,家长端用微信群人工沟通。结果是老师每天要拍几十张照片发群,家长信息淹没在聊天记录里,出了纠纷都找不到依据。所以最终方案还是"管理后台+家长小程序"双端结构。

1.3 功能模块划分:双端各司其职

整个系统的功能模块划分大致如下,这个划分也直接决定了后面的表结构和接口设计。

模块核心功能面向角色实现端
学生管理学生信息增删改查、分班、状态管理老师/管理员管理后台小程序
班级管理班级信息、师生绑定、容量设置管理员管理后台小程序
考勤签到入托签到、离托签退、记录查询老师/家长双端
接送管理接送人白名单、接送记录确认老师/家长双端
费用管理月费生成、缴费登记、欠费统计管理员/家长双端
请假管理请假申请、审批、缺勤同步家长/老师双端
公告通知发布公告、消息触达管理员/家长双端

每个模块的功能都不复杂,难点在于模块之间的数据联动。比如请假审批通过后,当天的考勤状态要自动标记为请假,不能算缺勤;缴费完成后,家长端的费用状态要实时刷新。

2. 核心表结构设计:从业务反推数据库,而不是先画ER图

2.1 主表设计:学生、班级、老师、家长四张核心表

很多新手做表结构时喜欢一上来就画ER图,结果画出来一堆关系,落库的时候却不知道怎么处理。我的做法正好反过来:把业务里的每一个功能点列出来,反推需要哪些表、哪些字段。

这个系统的核心主表有六张:

  • student:学生信息表,字段包括学生姓名、性别、出生日期、班级ID、入学日期、状态。特别要注意的字段是status,我用了tinyint类型,1代表在读,0代表退托,这样退托学生不会影响班级考勤统计。
  • class_info:托管班级表,包括班级名称、所属年级、负责老师ID、容量上限。这里有个业务细节,一个托管班通常对应一个主带老师,所以直接冗余了teacher_id,查询班级列表时少一次join。
  • teacher:教师表,包括登录账号、密码(BCrypt加密)、姓名、手机号、角色。角色字段我用的是普通字符串,虽然不够优雅,但胜在直观,项目里判断权限时直接用"ADMIN"、"TEACHER"比较。
  • parent_user:家长表,核心字段是openid,这是微信小程序登录后拿到的唯一标识。第一次通过微信授权登录时,系统会自动创建这条记录。
  • student_parent_rel:学生家长关联表,这个表单独说。
  • sign_record:签到记录表,后面详细说。

2.2 学生与家长的多对多关系:为什么必须建关联表

学生和家长之间不是简单的一对一。一个家庭可能有两个孩子都在同一个托管班,一个孩子也可能同时绑定父亲、母亲、爷爷奶奶四个监护人。如果只在student表里加一个parent_id字段,遇到多孩子或多家长的情况就直接崩了。

所以这里必须建关联表student_parent_rel,字段包括自增ID、student_id、parent_id、关系类型(爸爸/妈妈/爷爷/奶奶/其他)、是否接送授权人。其中"是否接送授权人"这个字段是我后来加上的,因为在接送场景里,不是所有绑定家长都有权接走孩子,得有一个明确的标记。

查询设计上,一个典型的接口逻辑是:家长登录后,先查关联表拿到所有student_id,再查student表列表。这里我用了一条SQL解决:

SELECT s.* FROM student s INNER JOIN student_parent_rel r ON s.id = r.student_id WHERE r.parent_id = #{parentId} AND s.status = 1

2.3 签到记录与接送记录:两个动作不能混在一张表里

入托签到和离托接送是两个完全不同的业务动作,虽然它们都发生在"学生进出托管班"这个时间点上,但记录的内容和用途差别很大。

签到记录侧重的是考勤:谁、几点到、几点走、当天状态(正常/迟到/请假/缺勤)。它面向的是老师和管理员,用于统计出勤率。

接送记录侧重的是安全与责任归属:谁来接的、和学生的关系是什么、哪个老师确认放行的。它面向的更多是安全事故追溯,出了问题能查清楚哪一环有漏洞。

我把这两张表分开设计,字段对比如下:

字段sign_record(签到记录)pickup_record(接送记录)
学生IDstudent_idstudent_id
动作类型sign_type(1入托/2离托)无(本身就是接送行为)
时间sign_timepickup_time
操作人operator_id(老师ID)confirm_teacher_id(确认老师ID)
关联人员无picker_name、picker_relation(接送人姓名与关系)
备注remarkremark

之所以要拆,还有一个现实原因:签到记录一天只有两条(入托+离托),但接送记录可能一天有多条,比如中午家长临时接走一小时内送回,这种特殊场景如果是单表design,记录会非常混乱。

3. 后端接口与小程序端的交互设计:登录、鉴权、请求封装

3.1 微信登录完整流程:code换openid,再换业务token

微信小程序登录和传统账密登录最大的区别,是它没有密码这个概念。整个流程基于微信的开放能力:

  1. 小程序端调用wx.login()拿到一个临时凭证code;
  2. 把code传给自己的后端接口/api/auth/login;
  3. 后端用code调用微信的jscode2session接口,换回用户的openid和session_key;
  4. 后端拿着openid查parent_user表,存在则直接生成业务token返回,不存在则自动创建新用户;
  5. 前端把token存到wx.setStorageSync里,后续所有请求都带这个token。

这里有个安全误区必须提醒:openid的换取必须放在后端完成。有些同学图方便,在小程序端直接请求微信接口换openid,这意味着你不得不在小程序代码里写死AppSecret,而小程序代码是半公开的,AppSecret一旦泄露,任何人都能伪造身份。

后端核心代码大致是这样:

@RestController @RequestMapping("/api/auth") public class AuthController { @Resource private ParentUserService parentUserService; @GetMapping("/login") public Result login(@RequestParam String code) { // 1. 调用微信接口换取openid WxLoginResult wxResult = wxService.code2Session(code); if (wxResult.getErrcode() != 0) { return Result.error("微信登录失败:" + wxResult.getErrmsg()); } // 2. 查询或创建家长用户 String openid = wxResult.getOpenid(); ParentUser parent = parentUserService.findByOpenid(openid); if (parent == null) { parent = parentUserService.createByOpenid(openid); } // 3. 签发业务token String token = JwtUtil.createToken(parent.getId(), parent.getRole()); return Result.ok(token); } }

3.2 拦截器鉴权与三种角色的权限控制

系统里一共三类使用者:管理员、老师、家长。接口不能谁都能调,比如家长不能调用签到接口,老师不能修改收费标准。

我的做法是后端统一加一个AuthInterceptor拦截器,所有/api/开头的请求都会先经过它。拦截器从请求头里拿token,校验签名和有效期,解析出用户ID和角色,放ThreadLocal里供后续Controller直接取用。

@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token)) { throw new BizException(401, "未登录"); } // 解析token,若过期或非法则抛异常 LoginUser user = JwtUtil.parseToken(token); UserContext.set(user); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); // 防止线程池复用导致用户串号 } }

角色控制我用了Spring拦截器的路径匹配来实现,简洁够用,不需要引入Spring Security那么重的框架。比如/api/teacher/**的请求必须角色为TEACHER或ADMIN,/api/admin/**必须为ADMIN。这种配置对一个毕设级别、访客量不大的系统是合适的,真要做大型生产系统再换Security做细粒度权限。

3.3 小程序端请求封装:统一域名、token注入与错误处理

小程序端的请求不能每次都写一遍wx.request,必须封装。我习惯在utils/request.js里统一管理。

const BASE_URL = 'http://192.168.1.100:8080/api' const request = (url, method, data) => { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method || 'GET', data: data || {}, header: { 'Authorization': wx.getStorageSync('token') }, success: (res) => { if (res.statusCode === 200 && res.data.code === 200) { resolve(res.data.data) } else if (res.data.code === 401) { wx.navigateTo({ url: '/pages/login/login' }) reject(res.data) } 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 }

有个细节:真机预览时,BASE_URL绝对不能写localhost。手机访问localhost指向的是手机自身,不是你开发用的电脑。要连电脑上起的后端,必须把BASE_URL改成电脑在局域网里的IP地址,比如http://192.168.1.100:8080/api。开发模式下微信开发者工具可以勾选"不校验合法域名",但真机预览需要在详情设置里打开调试模式,否则请求会被拦掉。

4. 几个容易写歪的业务功能:接送授权、缴费流水、请假状态机

4.1 接送人白名单:安全红线怎么用代码表达

接送场景的业务痛点在于:托管机构必须保证孩子不会被陌生人接走。所以设计上我做了接送人白名单机制。

家长在小程序端维护接送白名单,也就是student_parent_rel表里is_authorized_pickup=1的那些记录。每新增一个接送授权人,系统会要求填写姓名、关系、手机号。老师在小程序端执行离托操作时,需要在接送人列表里选择"此刻来接的是谁",并可以调起摄像头拍一张接送人的照片作为留存证据。

这个流程用后端接口表达如下:

POST /api/pickup/confirm 参数:studentId, pickerId, photoUrl, remark 逻辑: 1. 校验pickerId是否在studentId的白名单里; 2. 校验学生当前状态是否为"在托"; 3. 写入pickup_record表; 4. 将学生状态置为"已离托"。

第2步的校验特别重要。如果学生已经离托,是不能再次执行离托操作的,否则会出现"一个学生被接走两次"的数据脏账。我当时用一个整型状态字段current_status来标记学生当下是在托还是离托,1表示在托,0表示离托,签到时置为1,离托时置为0,并且通过事务保证这两步同时成功或同时失败。

4.2 费用流水表:不做余额字段,只做变动记录

费用模块一开始我犯过一个设计错误:想了直接在student表里加一个balance余额字段,每次缴费就加,每次计费就减。后来发现这会产生严重的数据一致性问题——退费、调价、优惠这些操作很难追溯,而且并发时容易算错。

后来我改成了纯粹的流水账设计:一张fee_record表,只记录每一笔费用的变动,不冗余任何"当前余额"。需要看某个学生的缴费情况时,就查这张表按时间排序。

费用记录表字段如下:

字段说明
id自增主键
student_id学生ID
fee_type费用类型:1月托费/2延时费/3餐费/4退费
amount变动金额,正数表示应收,负数表示退费
status状态:0待支付/1已支付/2已取消
create_time生成时间
pay_time支付时间
remark备注,比如"2024年3月托费"

为什么不做余额字段?因为托管班的收费场景相对简单,一个月就一笔托费,不像商城那样有充值、优惠、积分等复杂场景,流水账完全能支持所有查询需求,而且没有任何一致性问题。如果你发现项目要求里出现了"账户余额"这种概念,那才需要单独考虑钱包表,在此之前,流水账够了。

4.3 请假流程:一个简单的状态机,却有明确的业务闭环

请假功能看似只是"家长填个表单",但它牵涉到考勤统计的联动。我把它设计成三态状态机:

  • 申请中:家长提交后,状态为PENDING;
  • 已通过:老师审核同意后,状态为APPROVED,同时自动生成一条当天的考勤标记(状态为"请假"),不记入缺勤;
  • 已驳回:老师审核拒绝后,状态为REJECTED,此时家长可以修改日期后重新提交。

这个状态机的核心在于"已通过"时对考勤记录的操作。如果请假审核通过后,考勤表里没有同步标记,月底统计出勤率时就会把请假当缺勤,家长一投诉就是事故。所以这个联动必须放在请假审批通过的同一个事务里,不能靠定时任务去补。

请假表的表结构里还有一个隐藏字段:leave_date,记录的是具体请假日期。因为托管班的考勤按天算,一个请假申请只对应某一天,不做多日连请。如果家长想请三天假,就分别提交三条申请,这样后端逻辑简单,老师审批也清楚。

5. 项目跑通的全流程与典型报错:从零环境到双端联调

5.1 环境准备清单

这个项目后端依赖的核心技术栈是:JDK 8或17(看SpringBoot版本)、Maven 3.6+、MySQL 5.7或8.0;前端需要微信开发者工具。

JDK版本这块特别提醒一下:SpringBoot 2.x建议用JDK 8,SpringBoot 3.x必须用JDK 17。买的毕设代码如果用的是SpringBoot 2.7.x但本机装了JDK 17,大概率启动时没问题,但一旦项目里用了javax开头的包就会报ClassNotFoundException。先把项目里的pom.xml打开看一下spring-boot-starter-parent版本,再决定装哪个JDK,别拿到就急着去改环境变量。

MySQL方面建议直接用8.0,字符集建库时指定utf8mb4,避免学生姓名里有生僻字时出现乱码。建库语句:

CREATE DATABASE tuition_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

5.2 后端启动三步走

第一步,定基础环境。JDK配置好JAVA_HOME,Maven配置好settings.xml里的阿里云镜像,不然拉依赖能把人急死。Maven镜像配置如下,这是每个Java后端开发者必会的操作:

<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>aliyun maven mirror</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>

第二步,建库和执行SQL。项目源码里一般会带一个sql目录,里面是完整的建表脚本和初始数据。用Navicat直接执行就好。如果源码没带SQL文件,那要么是作者忘了打包,要么是用了MyBatis-Plus的自动建表插件,启动时自动生成,但这种情况反而是隐患。

第三步,改配置文件。打开src/main/resources/application.yml,把数据库账号密码、微信小程序AppID和AppSecret改掉。这里微信配置如果没填,登录接口就会报,所以你得先去微信公众平台注册一个小程序账号,拿到AppID。个人主体的注册就能拿到AppID,不需要认证。

启动成功后,控制台会出现SpringBoot的启动日志,访问http://localhost:8080/一般会返回404,这其实说明启动成功了,只是没有配根路径的Controller而已。

5.3 小程序前端配置与真机调试

打开微信开发者工具,导入小程序前端目录时,需要填写自己的AppID,这一点不要用测试号。测试号虽然能编译,但很多接口权限受限。

小程序端要改的位置通常只有一个:请求封装文件里的BASE_URL。调试阶段建议使用局域网IP加后端端口。比如电脑IP是192.168.1.100,那就把BASE_URL设为http://192.168.1.100:8080/api。

在开发者工具的"详情-本地设置"里,勾选"不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书"。这个选项是开发调试的生命线,不勾选的话,请求直接返回fail,而且错误信息藏在控制台里不够显眼,很容易让人误判成后端问题。

真机预览时,手机和电脑必须连同一个局域网Wi-Fi。如果手机能打开网页但小程序请求还是失败,检查一下电脑防火墙是不是拦了8080端口,临时关掉Windows防火墙再试一次,成功率会大幅提升。

5.4 我踩过的三个坑,以及对应的排查思路

第一个坑:LocalDateTime序列化格式问题

前端显示签到时间时突然变成了平平整整的数字字符串,比如2024-03-01T10:30:00,和预期格式差很远。这本质上是SpringBoot默认的Jackson序列化器对LocalDateTime的输出格式不符合我们的展示需求。

处理方式有两种:一是字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),二是全局配置。我推荐在application.yml里统一配置:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss

但这只对java.util.Date生效,对LocalDateTime不生效。LocalDateTime还是要加上@JsonFormat注解,或者引入jackson-datatype-jsr310模块并自定义序列化配置。最省事的做法是实体类字段上直接标注。

第二个坑:MyBatis-Plus逻辑删除与唯一索引的冲突

MyBatis-Plus的逻辑删除是加了一个deleted字段,查询时自动拼上WHERE deleted = 0。但如果某张表的某个字段设置了唯一索引,当过一年的学生退托被逻辑删除后,第二年重新录入一个同名同号的转学生,就会触发唯一索引冲突,因为逻辑上"删掉"的记录其实还在表里。

解决思路是在业务上绕开问题:比如对学生的唯一索引设计为student_no加class_id组合,或使用唯一索引加版本字段。如果毕设代码已经踩到这个坑,最简单的处理方式是把逻辑删除改成物理删除,毕竟学生数据恢复场景在托管系统里很少出现。

第三个坑:微信code2Session的errcode,尤其40029

微信登录第一次联调时一直报错,code换取openid失败,错误码是40029,也就是code无效。这个坑的原因是后端拿到的code已经被用过了,微信的code是一次性的。

常见的触发场景有两个:前端登录逻辑写了重试,或者后端接口被重复调用。比如页面onLoad时执行wx.login,onShow时又执行了一次wx.login,后一次拿到的code就会让前一次的失效。排查时可以直接在微信开发者工具的Network面板里看请求次数,一般立即就能发现问题。

最后再说几句大实话

做这类全栈管理系统,最耗时间的永远不是某个单一功能,而是数据流转与功能联动的设计。表结构没想清楚就动手写接口,后期一定会在联调返工上付出成倍的代价。先把业务场景走一遍,把所有需要记录的状态和关系列出来,再落数据库,这步做得越扎实,后面写代码越快。

如果你拿到的就是一套像这样带前后端的毕设源码,我建议不要急着直接跑起来看效果,先用Navicat把每个表都点开看一眼字段,再用微信开发者工具的调试器逐行追一遍登录流程。把这套系统真正弄懂,比单纯跑通拿到一个结果有用得多,答辩时老师随便提个问题你也能接住。

有个小技巧送给你:给老师演示系统前,把一些典型数据做齐——比如一个学生完整的签到离托记录、一笔缴费流水、一条请假审批记录——截好图,放在PPT里。老师不一定有时间看你现场操作,但截图能直观展示系统的业务完整性,这是你在答辩时最能拿分的部分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询