1. 三端分离架构的选型逻辑与整体设计
1.1 为什么是微信小程序、Node.js、Vue三端组合
做校园类系统,绕不开一个核心矛盾:用户群体分散在移动端和PC端,需求差异大,但开发资源往往有限。我试过用一套代码适配所有端,结果移动端体验拉胯,PC端管理功能又施展不开。后来转向三端分离,微信小程序负责学生和教师的移动端高频操作,Vue构建的Web后台负责管理员和教务人员的PC端复杂业务,Node.js作为中间层统一提供API并处理业务逻辑。这个组合不是拍脑袋定的,而是基于三个现实考量。
第一,微信小程序在校园场景的渗透率极高。学生不用装App,扫码或搜索就能用,课表查询、成绩查看、通知接收这些高频轻量操作,小程序体验足够好。第二,Vue的生态成熟,Element Plus或Ant Design Vue这类组件库能快速搭出管理后台,教务排课、数据统计、批量导入导出这些重交互功能,PC端大屏操作效率远高于手机。第三,Node.js作为BFF层,既能复用JavaScript技术栈降低团队学习成本,又能灵活聚合后端微服务或直接操作数据库,对于校园系统这种中等并发、业务逻辑中等复杂的场景,性能完全够用。
注意:三端分离不等于三套完全独立的代码。核心业务逻辑应当下沉到Node.js层,小程序和Vue端只负责展示和交互,避免同一套校验规则写三遍。
1.2 整体架构分层与数据流向
整个系统的架构可以拆成四层:客户端层、网关层、业务服务层、数据存储层。客户端层就是微信小程序和Vue Web应用;网关层用Nginx做反向代理和负载均衡,同时处理静态资源;业务服务层是Node.js集群,按业务域拆分成用户服务、课程服务、通知服务等模块;数据存储层用MySQL做主库,Redis做缓存和会话管理,文件资源走对象存储。
数据流向是这样的:小程序端发起请求,经过微信服务器转发到我们的Nginx,Nginx根据路径规则转发到对应的Node.js服务实例。Node.js服务先检查Redis中是否有缓存,有则直接返回,没有则查询MySQL,写入缓存后返回。Vue端的管理后台请求走同样的链路,但多了JWT鉴权和角色权限校验环节。这种设计的好处是,小程序端和Vue端共享同一套API,后端只需维护一份业务逻辑,前端各自按需调用。
1.3 技术选型背后的取舍与踩坑预判
选Node.js而不是Java或Go,主要考虑团队技术栈统一。如果团队本身是前端出身,Node.js的上手成本最低。但要注意,Node.js不适合做CPU密集型任务,比如大规模报表生成或复杂图像处理,这些应当拆成独立服务用其他语言实现。我们当时把成绩单PDF生成放在Node.js里做,结果并发一高就阻塞事件循环,后来改成用子进程调用外部工具才解决。
微信小程序端有个坑:不能直接使用npm包,需要用微信开发者工具构建npm,而且部分Node.js原生模块不支持。我们一开始想在小程序里直接做数据加密,发现crypto模块用不了,最后把加密逻辑放到Node.js层,小程序只传明文走HTTPS。Vue端相对自由,但要注意路由懒加载和打包体积,管理后台功能多了以后,首屏加载超过3秒用户就会明显感知卡顿。
2. 核心模块拆解与接口设计要点
2.1 用户体系与多端鉴权方案
校园系统的用户角色通常分学生、教师、教务管理员、系统管理员四类。微信小程序端用微信授权登录,拿到code后传给Node.js层,Node.js调用微信接口换取openid,再关联系统内的用户ID。Vue端用账号密码登录,Node.js验证后签发JWT。这里的关键是,两套登录体系最终要映射到同一个用户表,用unionid或手机号做关联。
JWT的payload里放userId、role、expireTime,不要放敏感信息。Token有效期设短一点,比如2小时,配合refreshToken做续期。小程序端把Token存在Storage里,每次请求放在Header的Authorization字段。Vue端可以存在Vuex或Pinia里,配合路由守卫做页面级权限控制。
实操心得:微信小程序的登录态维护有个细节,wx.checkSession判断 session_key 是否过期,但实际测试发现它并不完全可靠。更稳妥的做法是每次请求如果返回401,就静默重新走一遍登录流程,用户无感知。
2.2 课程与课表模块的数据结构设计
课表是校园系统里查询频率最高的功能之一。数据结构设计不好,后面查询和渲染都会很痛苦。我们的课程表设计是这样的:课程表存课程基本信息(课程名、教师、学分、周次范围),课程安排表存具体的时间地点(课程ID、星期几、第几节、周次、教室)。查询某学生某周课表时,先查学生选课关系,再关联课程安排,按星期和节次排序。
这里有个坑:单双周和调课逻辑。如果只在课程安排表里存一个周次字段,查询时要写复杂的SQL判断。我们的做法是,在课程安排表里加一个week_type字段,0表示每周,1表示单周,2表示双周,查询时用简单的条件过滤。调课则单独建一张调课表,记录原时间、新时间、生效周次,查询时做合并。
小程序端渲染课表用grid布局,每个格子对应一个时间槽。Vue端管理后台用表格展示,支持拖拽调课。两端的数据格式要统一,Node.js层返回的课表数据是一个二维数组,行是节次,列是星期,每个元素是课程对象或null。这样前端拿到就能直接渲染,不需要再做转换。
2.3 通知与消息推送的实时性处理
校园通知分两种:一种是系统公告,所有或部分用户可见;另一种是个人消息,比如选课结果、成绩发布。系统公告用轮询或WebSocket推送,个人消息用微信模板消息或小程序订阅消息。
我们当时选了WebSocket做实时通知,Node.js用ws库起一个WebSocket服务,小程序端用wx.connectSocket连接。但微信小程序对WebSocket连接数有限制,单个用户同时只能有一个连接,而且切后台一段时间后会断开。后来改成混合方案:重要通知走微信订阅消息,普通通知走WebSocket,WebSocket断了就降级为轮询。
Vue端的管理后台用WebSocket接收实时数据更新,比如在线用户数、待办事项提醒。这里要注意,WebSocket服务要和HTTP服务共享会话,我们用Redis存储session,WebSocket连接建立时从URL参数或Header里取Token,验证后把userId和socketId的映射存到Redis。
3. 实操过程与核心环节实现
3.1 Node.js服务端项目初始化与目录结构
先初始化项目,用npm init -y生成package.json,然后安装核心依赖:express或koa做Web框架,mysql2做数据库驱动,ioredis做Redis客户端,jsonwebtoken做鉴权,ws做WebSocket,dotenv管理环境变量。我习惯用express,生态最全,中间件多。
目录结构这样组织:
server/ src/ config/ 数据库、Redis、微信等配置 controllers/ 路由处理函数 services/ 业务逻辑 models/ 数据模型和SQL封装 middlewares/ 鉴权、日志、错误处理 routes/ 路由定义 utils/ 工具函数 app.js 入口文件 .env package.json入口文件里做几件事:加载环境变量、初始化数据库连接池、初始化Redis、注册中间件、挂载路由、启动HTTP服务和WebSocket服务。数据库连接池大小根据并发量调,校园系统一般设10到20就够。Redis用来存session和热点数据,比如课表查询结果缓存5分钟。
注意:Node.js服务不要用root用户跑,用pm2做进程管理,配置max_memory_restart防止内存泄漏拖垮服务器。日志用winston或pino,按天切割,方便排查问题。
3.2 微信小程序端核心页面与请求封装
小程序端用原生开发或uni-app都行,我们选了原生,因为对微信API的兼容性最好。核心页面有:登录页、首页、课表页、成绩页、通知页、个人中心。每个页面在onLoad时检查登录态,未登录跳转到登录页。
请求封装是关键。写一个request.js,统一处理baseURL、Token注入、错误提示、loading状态。代码大概这样:
const request = (options) => { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token'); wx.request({ url: `${baseURL}${options.url}`, method: options.method || 'GET', data: options.data || {}, header: { 'Authorization': token ? `Bearer ${token}` : '', 'Content-Type': 'application/json' }, success: (res) => { if (res.statusCode === 401) { // 重新登录逻辑 wx.removeStorageSync('token'); wx.redirectTo({ url: '/pages/login/login' }); reject(new Error('未登录')); } else if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else { wx.showToast({ title: res.data.message || '请求失败', icon: 'none' }); reject(new Error(res.data.message)); } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); };课表页渲染用wx:for循环二维数组,每个格子根据课程颜色做区分。成绩页用图表展示,可以用echarts-for-weixin,但要注意包体积,建议按需引入。
3.3 Vue管理后台的搭建与权限路由
Vue端用Vue3 + Vite + Pinia + Vue Router + Element Plus。初始化项目后,先配路由和权限。路由分两种:静态路由(登录页、404页)和动态路由(根据用户角色动态添加)。用户登录后,Node.js返回角色和权限列表,前端根据权限过滤路由表,用router.addRoute动态添加。
权限控制到按钮级别,用自定义指令v-permission,传入权限码,没有权限则隐藏元素。比如:
app.directive('permission', { mounted(el, binding) { const permissions = store.getters.permissions; if (!permissions.includes(binding.value)) { el.parentNode.removeChild(el); } } });管理后台的核心页面有:用户管理、课程管理、课表管理、通知管理、数据统计。数据统计页用echarts,注意按需引入图表类型,不然打包体积会很大。表格分页用Element Plus的el-pagination,后端接口支持page和pageSize参数。
实操心得:Vue管理后台的接口请求用axios,配拦截器统一处理Token和错误。开发环境配proxy解决跨域,生产环境用Nginx反向代理。打包时用vite的build配置做代码分割,把echarts、element-plus这些大库单独打包,利用浏览器缓存。
3.4 三端联调与接口契约管理
三端联调最容易出问题的环节是接口字段不一致。我们的做法是,先用Swagger或Apifox定义好接口契约,包括URL、方法、请求参数、响应格式、错误码。Node.js层按契约实现,小程序和Vue端按契约调用。联调时用Apifox的Mock功能,前端可以先不依赖后端,用Mock数据开发。
联调顺序建议:先调通登录和用户信息接口,再调课表和成绩接口,最后调通知和统计接口。每调通一个接口,用Postman或Apifox保存用例,方便回归测试。跨域问题在开发环境用proxy解决,生产环境用Nginx配置。
微信小程序端有个特殊问题:开发工具里可以关闭域名校验,但真机测试必须用HTTPS域名,而且域名要在微信公众平台配置。我们当时用内网穿透工具做真机调试,但要注意合规性,正式环境必须用备案域名和HTTPS证书。
4. 常见问题与排查技巧实录
4.1 接口联调中的典型报错与解决
联调阶段最常见的问题我整理成了一张速查表:
| 报错现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 401 Unauthorized | Token过期或未传 | 检查Header里Authorization字段 | 重新登录或刷新Token |
| 403 Forbidden | 角色权限不足 | 检查JWT里的role和接口要求的权限 | 调整角色权限配置 |
| 跨域错误 | 开发环境未配proxy | 看浏览器控制台Network面板 | 配Vite proxy或Nginx |
| 小程序请求失败 | 域名未配置或非HTTPS | 看微信开发者工具控制台 | 配置合法域名和证书 |
| 数据库连接超时 | 连接池耗尽或网络问题 | 看Node.js日志和MySQL状态 | 调大连接池或检查网络 |
| WebSocket频繁断开 | 心跳未配置或超时 | 看小程序端onClose回调 | 加心跳包和重连逻辑 |
其中跨域问题最容易被忽视。开发环境Vite的proxy配置大概这样:
// vite.config.js export default { server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } };4.2 性能瓶颈的定位与优化
校园系统在选课和查成绩的高峰期,并发量会突然飙升。我们遇到过MySQL CPU打满的情况,排查后发现是课表查询没走索引。课程安排表的student_id和week字段没建联合索引,导致全表扫描。加上索引后,查询时间从2秒降到50毫秒。
Redis缓存也要注意雪崩问题。我们给课表缓存设置了随机过期时间,比如300秒加减60秒的随机值,避免同一时间大量缓存失效。另外,Node.js层用cluster模式启动多进程,充分利用多核CPU。pm2配置instances为max,自动根据CPU核数启动进程。
小程序端的性能优化主要是减少setData的数据量和频率。课表页渲染时,不要一次性setData整个二维数组,可以分块渲染,或者用wx:key优化列表更新。Vue端的优化主要是路由懒加载和组件按需引入,首屏只加载必要资源。
4.3 数据一致性与事务处理
校园系统里,选课和退课涉及多个表的更新,必须用事务保证一致性。Node.js里用mysql2的connection.beginTransaction()开启事务,执行多条SQL后commit,出错则rollback。注意,事务里的操作要尽量短,避免长时间锁表。
还有一个坑是并发选课。同一门课名额有限,多个学生同时选,如果不做并发控制,会出现超选。我们的做法是在课程表里加一个selected_count字段,选课时用UPDATE course SET selected_count = selected_count + 1 WHERE id = ? AND selected_count < capacity,根据affectedRows判断是否选课成功。这样利用数据库的行锁保证原子性,比在应用层加锁更可靠。
注意:微信小程序的订阅消息需要用户主动授权,而且每次授权只能发一条。重要通知要提前引导用户授权,或者用服务号模板消息作为补充。但服务号需要用户关注,转化率是个问题,实际项目中要权衡。
4.4 部署上线与灰度发布策略
部署用Docker Compose编排Node.js服务、MySQL、Redis、Nginx。Nginx配置反向代理和静态资源服务,Vue打包后的dist目录挂到Nginx里。小程序端通过微信开发者工具上传代码,提交审核后发布。
灰度发布很重要。我们当时先让一个班级试用,观察一周没问题再逐步放开。Node.js服务用pm2的reload做平滑重启,避免服务中断。数据库变更用迁移脚本,每次上线前先备份,变更脚本要可回滚。
监控用pm2的monitor和日志,配合简单的健康检查接口。如果Node.js服务挂了,Nginx会返回502,我们配了告警通知到工作群。Redis和MySQL用云服务商的监控面板,设置CPU和连接数告警阈值。
整个项目从零到上线大概用了两个月,其中联调占了一半时间。最大的体会是,接口契约一定要提前定好,不然后端改一个字段,前端三端都要跟着改。另外,微信小程序的审核周期不可控,重要功能要提前预留审核时间。这个架构后续还可以扩展,比如接入校园一卡通数据、图书馆座位预约、宿舍报修等模块,Node.js层按业务域拆分成微服务,小程序端用分包加载控制主包体积。