每年到开学季,总有一批学生要在报到现场排长队:填表、交材料、核对信息,每个环节都堆着人。我做这个学生预注册系统的原因很简单——把报名流程中那些重复、机械的采集和确认工作提前拆掉,让学生在到校之前就通过小程序完成信息提交和资格预审,后台管理人员只需在电脑上批量审核。说直白点,就是用一个前后端分离的 Web + 小程序方案,把"现场排队报名"变成"线上提前注册"。
这套系统我选型时定的组合是 Spring Boot + 微信小程序 + Vue,整体前后端分离。文章适合正在做毕业设计或课程项目的开发者参考,也适合想从零理清"小程序端、管理后台、后端API"三者如何协作的同学。我会把业务设计、数据库结构、接口实现、双端联动、联调部署这些环节的关键点和坑都过一遍,尽量做到拿到题目就能照着自己搭。
1. 预注册不是"报个名"那么简单:先看清业务本质
1.1 预注册的业务流程与真正的核心难点
很多同学看到"学生预注册"这个题目,第一反应是做一张表单、存一条记录,再让管理员能查到。这样想就偏了。预注册的本质是提前完成资格确认和分批引导,它比"填表存库"多了一层审核流转。
一个比较完整的预注册流程是这样的:
- 学院或教务人员先创建"注册批次",比如"2025年秋季本科新生预注册",设置批次开放时间、截止时间、可填报人数上限。
- 学生在微信小程序里选择对应批次,填写个人基础信息、上传证件材料照片,提交后进入"待审核"状态。
- 管理端(Vue后台)按批次查看学生列表,审核每条记录,标记"通过"或"驳回",驳回时通常要填原因。
- 小程序端实时获取审核状态,学生看到通过结果后完成后续操作(比如确认到校时间、预选宿舍意向等),状态变为"已确认"。
- 管理员按批次导出报名数据,用于后续分班、住宿安排。
这里真正的难点不是增删改查,而是状态机的完整闭环。如果只是存一个字符串字段,审核驳回之后学生改信息再提交,状态怎么回退?批次关闭了还能不能提交?管理员正在审核的时候学生把信息改了怎么办?这些细节才是系统能不能落地的关键。
1.2 为什么选 Spring Boot + 微信小程序 + Vue 这套组合
技术选型不是越新越好,而是要看开发效率、社区资料和团队熟悉度。我最终定下这套组合,有下面几点考虑。
第一,Spring Boot 对于中小型业务系统来说是性价比很高的后端框架。自动配置减少了大量 XML 配置,内嵌 Tomcat 让打 jar 包部署变得非常简单,Spring Data JPA 或 MyBatis 偷懒都行,生态成熟,遇到问题很容易搜到解决方案。对于时间紧的毕设项目,这一条就足够有吸引力。
第二,微信小程序是学生端最合理的载体。现在的学生手机里必然有微信,小程序用完即走,不需要安装 App,也不像 H5 那样要收藏网址。它天然具备微信授权的身份标识能力,一个wx.login就能拿到用户唯一标识,免去了自己设计账号体系的麻烦。
第三,管理端用 Vue 做前后端分离,是考虑到后台操作界面需要频繁迭代。用 Vue 写管理页面非常顺手,列表、表单、筛选、弹窗这些组件生态齐全,开发速度比在服务端里塞一个模板引擎快得多。前后端分离后,后端只需要输出 JSON 接口,小程序端和管理端都通过 HTTP 调用同一套 API,代码复用率高。
注意:选择"前后端分离"并不仅仅是为了写起来爽,它意味着你要提前设计好接口规范、Token 鉴权方案和跨域处理。这些体积会在联调阶段集中爆发,后面我会专门讲。
2. 数据库设计:把注册流程"固化"成一张状态网
2.1 核心表结构拆解与字段设计思路
数据库设计这个环节,我建议先画清楚业务对象,再落表。预注册系统的核心对象主要有五类:批次、学生用户、预注册申请记录、审核日志、通知记录。下面是我在这套系统里实际使用的核心表设计。
先看注册批次表register_batch:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| batch_name | varchar(100) | 批次名称,如"2025年秋季预注册" |
| start_time | datetime | 开放时间 |
| end_time | datetime | 截止时间 |
| register_limit | int | 人数上限,默认0表示不限 |
| status | tinyint | 批次状态:1草稿,2已发布,3已结束 |
| create_time | datetime | 创建时间 |
再看用户主表student_user:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| openid | varchar(64) | 微信 openid,唯一索引 |
| real_name | varchar(50) | 姓名 |
| id_number | varchar(20) | 身份证号 |
| phone | varchar(20) | 手机号 |
| avatar_url | varchar(255) | 头像地址 |
| role | tinyint | 1学生,2管理员 |
| create_time | datetime | 创建时间 |
然后是核心的预注册申请表register_apply:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| batch_id | bigint | 关联批次ID |
| student_id | bigint | 关联学生用户ID |
| student_type | tinyint | 新生/转校生/往届 |
| major_id | bigint | 意向专业ID |
| status | tinyint | 1草稿,2已提交待审核,3审核通过,4审核驳回,5已确认 |
| ext_info | json/text | 扩展信息,存各批次自定义字段 |
| reject_reason | varchar(255) | 驳回原因 |
| submit_time | datetime | 提交时间 |
| audit_time | datetime | 审核时间 |
最后是一张审核日志表audit_log和一张通知记录表notify_log,分别用来记录每次状态变动和给用户发送的消息。
这里有几个容易踩的坑,我提醒一下:
- 不要把
ext_info设计成几十个独立的可空字段。不同批次的表单字段可能完全不一样,灵活的做法是存 JSON 字符串,查询时如果需要按具体字段筛选,再考虑拆列或建索引。 - 身份证号、手机号这类敏感字段建议单独处理,如果需要明文展示,至少做好权限控制,返回接口时不带多余信息。
openid必须加唯一索引,因为同一个微信号同一个小程序只会对应一个 openid,它天然是用户表的业务主键。
2.2 注册状态流转:为什么用整数字段而不是复杂状态表
状态流是预注册系统里最容易写乱的地方。我用了status整数段,整个系统大概有五种状态:
- 草稿:学生填了一部分还没提交,可以随时修改。
- 已提交待审核:学生点了提交,管理员可见,学生自己不可再改。
- 审核通过:管理员审核通过,系统自动写入通过时间。
- 审核驳回:管理员填了驳回原因,学生可以修改后重新提交(状态回到2)。
- 已确认:学生看到审核通过后,在小程序上点击"确认并补充信息",完成最终注册。
为什么不设计一张状态流转表?因为对于这种规模的系统,多一张状态表并不会带来业务上的便利,反而让每次状态更新都变成一次多表写入。用一个字段存当前状态,配合audit_log记录历史记录,已经足够支撑回溯需求。如果未来要做复杂的审批流、多人协作审核,再考虑引入状态机框架也不迟。
我在实际编码时还在 Service 层加了一个checkStatusChange(current, target)的校验方法,专门判断状态迁移是否合法。比如"审核通过"状态下不允许学生直接修改信息重新提交,"草稿"状态下管理员不需要看到待办列表。这样做的目的是把状态规则收敛到一个地方,避免 Controller 里到处都是if (status == 3)的散装判断。
3. 后端实现细节:登录鉴权与表单数据的"安全性错觉"
3.1 小程序登录态的设计:openid 换 token,而不是存明文
小程序端和后端交互时,绕不开微信登录。很多人第一次写这个会对wx.login有误解——code不是后端能直接使用的身份信息,它必须通过后端调用微信接口换取openid和session_key,然后后端再生成自己的会话凭证。
在我的实现里,小程序打开后第一步是wx.login拿到 code,然后调用后端接口/api/auth/login,后端通过HttpClient或 Hutool 工具类请求微信官方接口jscode2session,拿到openid和session_key。这个openid查用户表,如果不存在则自动注册一个基础用户并返回token;如果存在则直接生成token返回。
关键点是:**session_key绝对不要返回给前端**。它是用来解密手机号等敏感信息的密钥,一旦泄露,整个会话都不安全。后端只需要把openid跟自己的用户表绑定,然后签发一个自己的加密 Token 就行。
这里我用的是两层 Token 方案:
- 学生端登录后拿到
student_token,有效期设为 2 小时,后续请求在请求头X-Token中携带。 - 管理端走的是独立的账号密码登录,签发
admin_token,有效期 12 小时。
后端用一个拦截器统一处理:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("X-Token"); if (token == null || !jwtUtil.verifyToken(token)) { response.setStatus(401); return false; } String role = jwtUtil.getRole(token); if (!"admin".equals(role)) { response.setStatus(403); return false; } request.setAttribute("userId", jwtUtil.getUserId(token)); return true; }这样管理端接口加了/api/admin/**前缀统一做角色校验,学生端接口走/api/student/**做基本登录校验,权限边界非常清晰。
3.2 表单校验和文件上传:不能信任"前端已经校验过了"
预注册系统里免不了要上传证件照、头像、体检表等图片。小程序端通常用wx.chooseImage选图后通过wx.uploadFile直接传到后端接口。这里有一个很常见的坑:图片体积和类型不能只靠前端控制。
一个wx.uploadFile默认上传的文件,如果后端使用 Spring 的MultipartFile接收,默认单文件最大是 1MB。学生手机拍的照片动辄三五兆,直接报错。我的做法是在配置类里调大限制:
spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB但调大不等于不限制,我还在后端做了两次校验:第一次在进入 Controller 之前,用拦截器判断文件后缀名是否在白名单内(jpg、png、pdf),第二次在业务代码里用第三方工具类读取文件头判断真实格式。为什么做两次?因为修改后缀名绕过校验是入门级攻击手段,直接读文件头最保险。
表单本身的校验同样不能省。小程序端填了姓名和身份证号,后端还要再做一次正则校验,身份证号格式、手机号格式都要查。我在 Service 层封装了一个ValidateUtils,专门处理这些规则。有人觉得重复校验啰嗦,但线上的真实请求可不都来自你的小程序,用 Postman 直接发请求绕过前端太简单了。
4. 双端联动:小程序端与 Vue 管理端怎么配合
4.1 管理后台的功能布局与接口调用封装
Vue 管理端我使用的是 Vue 2 + Element UI 的组合,页面主要分三类:批次管理、申请审核、数据统计。
批次管理页负责增删改查注册批次,核心操作是"发布批次"和"结束批次"。考虑到批次时间必须由后端校验,前端表单提交的数据只做展示性校验,真正的时间逻辑判断放在 Service 层。比如发布一个批次时,后端必须检查当前时间是否在start_time和end_time之间,防止有人通过接口直接造一个过期批次。
申请审核页是整个后台的流量中心。列表按批次过滤,默认展示"待审核"状态的学生记录,每条记录右侧有"通过"和"驳回"两个按钮。驳回时弹出dialog要求填写原因,原因不为空才能提交。这里有一个细节:审核通过和驳回都是覆盖式操作,操作前必须弹二次确认框,防止误触。
接口请求的封装上,我统一在src/utils/request.js里基于 axios 封装了一个实例:
service.interceptors.response.use( response => response.data, error => { if (error.response.status === 401) { localStorage.removeItem('admin_token'); router.push('/login'); } Message.error(error.response.data.message || '请求失败'); return Promise.reject(error); } );这样所有接口只需要关心业务数据,鉴权失败和错误提示统一处理,代码量减少很多。管理端用到的接口前缀是/api/admin/batch、/api/admin/apply这类,跟学生端天然隔离。
4.2 小程序端的页面流转与状态同步
小程序端按照"登录 - 批次列表 - 填写信息 - 提交结果 - 审核状态"五步设计页面。
登录页不需要用户输入账号密码,进入时静默调用wx.login,登录完成后把 token 存到wx.setStorageSync。其实这一步可以在 App 启动时刻完成,不必占用一个独立页面。品牌形象好的做法是做一个启动封面页,等待登录成功后自动跳转。
填写信息页是小程序端最复杂的页面,因为要适配不同的批次字段。我的做法是:
- 进入页面时请求
/api/student/batch/{batchId}/form-config,后端返回该批次需要展示的字段列表及类型(文本、下拉、日期、文件上传)。 - 前端根据字段配置动态渲染表单组件。
- 提交时前端把表单数据统一组装成
ext_infoJSON 对象传给后端。
这样带来的最大好处是:新增一个批次不用重新发布小程序,后端配置好字段就能用。
提交结果页展示两种状态:审核中(展示"预计 X 个工作日内审核")和审核完成(展示通过或驳回原因)。如果被驳回,页面上要有"重新修改"按钮,点击后回到填写信息页,此时所有字段自动回填上次已保存的数据。
审核状态同步我做了两件事:提交成功后的页面上放一个onPullDownRefresh下拉刷新,随时拉最新状态;小程序 App 的onShow事件里也触发一次状态查询接口,保证从旧页面切回来时数据不过期。不要依赖 WebSocket 推送,毕竟小程序后台消息推送限制很多,学生端的轻量轮询已经足够。
5. 一路踩过来的坑:联调、跨域、域名与真机调试
5.1 本地联调时的跨域处理方案
前后端分离项目在本地开发时,小程序开发者工具里可以勾选"不校验合法域名",但管理端 Vue 开发服务器和后端 Spring Boot 之间依然存在跨域问题。
我的做法是开发环境用 Vue CLI 的devServer.proxy转发,生产环境用 Nginx 配置反向代理,同时后端也配置了全局 CORS 过滤器,双保险。
Vue 配置如下:
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } };后端 CORS 配置:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }这里有一个细节值得注意:allowedOrigin("*")和allowCredentials(true)不能同时开,浏览器会拒绝带凭证的跨域请求。Spring Boot 新版本里addAllowedOrigin("*")配allowCredentials(true)大概率报错,要改用addAllowedOriginPattern("*")搭配处理,我在旧版本项目上踩过一次,排查了半个小时才发现是这里的问题。
5.2 小程序上线前的域名与 HTTPS 配置
小程序线上环境对请求域名有严格限制:必须是 HTTPS,且域名必须在小程序后台配置为 request 合法域名。这意味着后端上线不能直接暴露 IP 加端口,需要一套完整的域名和证书。
我第一次部署用的方案是:
- 云服务器上装 Nginx,监听 443 端口。
- 配置 SSL 证书(现在有免费的 DV 证书可用)。
- Nginx 把
/api/前缀的请求反向代理到内网的 Spring Boot 8080 端口。
Nginx 配置核心部分:
server { listen 443 ssl; server_name your-domain.com; ssl_certificate /etc/nginx/cert/fullchain.pem; ssl_certificate_key /etc/nginx/cert/privkey.pem; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署完成后,小程序开发者工具里还需要手动把域名添加到"request 合法域名"列表,上传体验版之前先做一次真机预览。真机调试和开发者工具里的最大差异是网络环境:工具里勾选"不校验合法域名"能跳过 HTTPS 校验,真机上不行,所以很多问题真机才暴露。建议提前准备一个二级域名专门用于测试,避免上线前手忙脚乱。
上传小程序代码时还要注意,小程序包体不能超过 2MB。如果图片资源很多,要么走云端文件地址,要么精简静态资源。我用的是把证件照等上传文件直接存服务器本地目录,数据库只存 URL,这样小程序包体只包含页面代码,绝不会超限。
5.3 几个容易忽略的经典问题
最后整理几个实际项目中非常容易出现的问题:
- 时间问题:小程序端展示时间要格式化时区,后端存 UTC 或本地时间必须达成一致。我统一用
LocalDateTime存数据库,返回给前端的 JSON 统一格式化为字符串。 - 并发重复提交:学生双击提交按钮,可能产生两条申请记录。我在
register_apply表对student_id + batch_id加唯一索引,从数据库层面兜底,同时在提交接口进入时先查一次是否已有正在审核的记录。 - 数据库连接池默认配置:Spring Boot 2.x 默认 HikariCP 连接池很稳,但默认池大小可能不够用。如果是多人和小程序并发测试,建议在配置里明确设置
minimum-idle和maximum-pool-size,避免高峰期连接等待超时。 - 日志记录:审核操作必须记录操作人,不只是靠
audit_log表,还要在应用日志里把完整的请求参数和结果打出来。遇到争议时可以快速定位到底是谁在什么时间改了什么内容。
6. 可扩展的方向与实际操作体会
这个预注册系统做完后,我又补了几个比较实用的扩展点,如果你时间充裕,可以试着加上去。
一是导出 Excel 功能。管理员审核完所有学生记录后,需要按批次导出名单。直接用 Apache POI 生成 XLSX 文件,后端接口/api/admin/apply/export返回文件流,Vue 端用blob下载。这个功能在实际办公场景里几乎是刚需,加上之后系统完整度提升很大。
二是订阅消息提醒。微信小程序可以发送一次性订阅消息,审核通过后触发通知,学生微信上收到"您的预注册审核已通过"的提醒。这里要注意的是,订阅消息需要用户提前在小程序里点击授权按钮,且一次性订阅只能发送一次对应模板消息,设计流程时要把授权时机放在"提交申请"之后。
三是数据统计看板。管理端首页放几个统计卡片:总报名人数、各专业报名分布、待审核数量、通过率。SQL 用group by统计即可,前端用 ECharts 画几个基础图表,整个系统的演示效果会明显上一个档次。
我在实际带项目时经常遇到的一个问题是:开发者把大量精力花在花哨的页面上,却忽略了业务逻辑的严谨性。比如审核驳回后学生能不能重新提交,这个看起来不显眼的功能,实际用起来却发现状态被卡死,只能改数据库手动处理。我的建议是,动手写代码之前先把状态流转图画出来,哪怕只画在纸面上,都要比边写边想清晰得多。
这套系统的实现难度属于中等偏上,但因为它完整覆盖了小程序端、管理端、后端、数据库、部署五大环节,做完之后对整个前后端分离开发流程的理解会非常扎实。如果你正好是第一次接触微信小程序,建议先单独跑通一个登录接口,确认能拿到 openid 再开始写业务代码,否则很容易上来就被登录流程卡住,后面越做越没信心。
我个人最后一点体会是:做这类系统,先做"丑但完整"的闭环,再做"好看但缺功能"的壳子。预注册的核心价值在于流程的完整性——从学生注册、管理员审核、到状态同步缺一不可。把这条主线跑通之后,再去打磨按钮样式、页面动画,整个项目的节奏就会从容很多。