开头
今年上半年接了一个中学生社团管理系统的小需求,技术栈定在 Node.js + Vue + 微信小程序。做之前我以为这种校园管理类项目无非就是“增删改查”四件套,真正动手之后才发现,坑全藏在细节里:三方角色权限怎么设计、审批流怎么走、小程序端和 Web 管理端怎么共用一套接口、上传视频和 m3u8 播放怎么兼容不同手机……
这篇文章就把整个项目从 0 到 1 的过程做一个复盘梳理,重点放在需求拆解、后端接口设计、小程序端 Vue 语法的落地方式,以及我在实际联调中踩过的高频问题。如果你正在做类似的学生管理系统、校园小程序,或者准备用 Node.js + Vue 做一个前后端分离的微信小程序项目,这篇内容可以直接帮你少走不少弯路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1. 项目整体拆解:先别急着写代码,把业务走通
很多人拿到项目第一件事就是建工程、装依赖,我习惯先花半天把用户故事画一遍。因为像社团管理系统这种项目,看起来只是“把线下报名搬到线上”,但实际涉及的场景很杂:学生浏览社团、提交入社申请、社团负责人审核、管理员发布活动、活动报名统计、社团动态发布、甚至还有视频展示和文件下载需求。你不把流程理清楚,表结构设计出来就是返工重来。
1.1 业务角色与需求闭环
这个系统里一共有三类角色:学生端、社团负责人端、系统管理员端。
- 学生端:浏览社团列表与详情、查看社团动态、提交加入申请、报名社团活动、查看自己的申请记录。
- 社团负责人端:管理本社成员的入社申请、发布活动、发布动态、查看报名成员、管理本社资料(比如图标、简介、展示视频等)。
- 系统管理员端:主要管人和管全局内容——审核社团创建申请、管理用户账号、处理举报/违规动态、查看全站统计等。
整个核心闭环就是“加入社团、参与活动、内容沉淀”三个动作。你可以把它理解成一个小型校园版“社群工具”,只是用户不会那么多,也不需要过于复杂的推荐流,但审批流的严谨性一定得有,否则会出现“谁都能改动态、谁都能乱加人”的情况。
我建议第一次画流程的时候就用最原始的纸笔,别一上来画特别复杂的图。把每个角色能做的事列出来,然后找交集,交集就是你接口文档里最核心的 CRUD。
1.2 技术选型:为什么是 Node.js + Vue + 小程序
整个项目选型非常明确:后端 Node.js 提供 API,前台管理端用 Vue 写一个 Web 后台,学生用的端口则以微信小程序承载。
后端用 Node.js 而不是 Java/Spring Boot,核心原因有两个:一是前后端语言统一,团队不用同时维护两套编程心智,前端同学也能看懂后端代码;二是这种校园管理系统都是中小并发量场景,Node.js 的异步 I/O 和 JSON 数据交互在读写轻逻辑业务时开发速度极快。配合 Express,几百行代码就能把整套用户鉴权和社团 CRUD 搭起来。
小程序为什么不用原生 WXML 而走 Vue 的写法?原因很好理解——如果单独用原生语法开发,管理端和小程序端实际上要维护两套完全不同的 UI 代码。借助 Vue 语法的跨端编译方案(我用的是 uni-app,实际写起来更接近 Vue 单文件组件的体验),同一套代码逻辑可以跑在小程序端,也能为后续需要时保留打包成 App 或者 H5 的余地。后面的实操我会重点讲 uni-app 和原生小程序的差异,因为这是最容易被新手低估的部分。
2. 后端服务设计:让 Node.js 当好“中转大脑”
一个稳定的 Node.js 服务,不在于用了多少框架、引了多少中间件,而在于目录结构清爽、中间件职责单一、错误处理统一。新人经常犯的毛病是把所有逻辑堆在 app.js 或 server.js 里,洋洋洒洒一千行,看着能跑,后面维护一次就想重构一次。
2.1 后端工程结构与启动流程
我的示例工程结构大致如下,你可以根据自己的项目名称调整:
text复制server/
├── app.js // 入口文件,初始化 express
├── config/
│ ├── index.js // 环境变量汇总
│ └── db.js // 数据库连接
├── routes/
│ ├── auth.js // 登录/注册/令牌刷新
│ ├── clubs.js // 社团相关接口
│ ├── activities.js // 社团活动接口
│ ├── applications.js // 申请审批接口
│ └── upload.js // 文件上传接口
├── controllers/ // 实际业务逻辑层
├── models/ // Sequelize/Mongoose 模型定义
├── middlewares/
│ ├── auth.js // JWT 校验
│ └── errorHandler.js // 统一错误处理
├── utils/
│ └── response.js // 统一响应辅助函数
└── package.json
入口文件不要塞业务代码,只做三件事:加载环境变量、挂载路由、连接数据库并开启端口。实现大概是:
javascript复制// app.js
const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
dotenv.config();
const { sequelize } = require('./config/db');
const routes = require('./routes');
const { errorHandler } = require('./middlewares/errorHandler');
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 静态资源映射,上传的图片/视频走这里
app.use('/uploads', express.static('uploads'));
// 业务路由
app.use('/api', routes);
// 统一错误处理
app.use(errorHandler);
const PORT = process.env.PORT || 3000;
sequelize.sync().then(() => {
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
});
这里要注意几个容易被坑的点:
sequelize.sync()在开发环境偶尔做表结构同步是够用的,但生产环境不要让它自动改表结构,否则跑一次可能把你精心设计的外键和索引全部重建一遍。建议平时把sync({ alter: false })关掉。- 跨域中间件必须加上。小程序端请求后端时其实不受浏览器跨域限制,但你的 Vue 管理后台是用浏览器打开的,不带
cors()的话,Web 端调试时你就会看到经典的 “CORS error”。
2.2 JWT 登录态设计:学生、负责人、管理员如何区分
校园类项目和普通 To C 应用有一个巨大区别:注册环节一般需要邀请制或管理员导入,而不是完全开放注册。所以我把登录设计成两步走:第一步,账号密码登录(也可以对接微信授权登录);第二步,后端根据账号的角色信息派发不同权限级别的 JWT。
实现逻辑大致如下:
javascript复制// middlewares/auth.js
const jwt = require('jsonwebtoken');
function signToken(payload) {
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '7d' });
}
function authRequired(req, res, next) {
const header = req.headers.authorization || '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) {
return res.status(401).json({ code: 401, message: '未登录或登录已过期' });
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (e) {
return res.status(401).json({ code: 401, message: 'token 无效' });
}
}
function roleRequired(...roles) {
return (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ code: 403, message: '没有权限执行该操作' });
}
next();
};
}
module.exports = { signToken, authRequired, roleRequired };
接口层就可以这样组合:
- 修改自己资料:
authRequired - 审核入社申请:
authRequired+roleRequired('admin', 'leader') - 删除社团动态:
authRequired+roleRequired('admin')
角色统一用字符串表示:student 学生、leader 社长、admin 管理员。如果角色数量再多,建议抽权限表出来做细粒度控制,但现在三个角色直接用字符串判断,简单高效,别过度设计。
2.3 统一返回体:小细节,大价值
很多项目接口数据格式经常是“这个人返回 { data: xxx },那个人返回 { list: xxx }”,前端联调的时候要一直问后端“这次是什么结构”。我一开始就定了一个统一标准:
json复制// 成功
{ "code": 0, "data": {}, "message": "success" }
// 失败
{ "code": 40001, "data": null, "message": "该社团不存在" }
这样前端代码里只需要封装一次 request 拦截器,所有逻辑都走同一个出口。微信小程序的 wx.request 和 Vue 管理端用的 axios 都只是在处理同一个结构,接口联调成本大幅下降。
建议把错误码先规划好。我的习惯是:0 表示成功,400xx 表示客户端参数问题,401xx 表示登录态问题,403xx 表示权限问题,500xx 表示服务端逻辑异常。有了这套约定,排查问题的时候看返回码就知道是前端参数传错了还是后端逻辑写崩了。
3. Vue 与小程序的共存:用 uni-app 写小程序的核心心得
很多人一看到标题就疑惑:Vue 和小程序是两个不同技术栈,怎么能写在一起?这里做一个基于实操的解释:小程序端并不是原生开发,而是基于 Vue 语法规范的跨端框架,比如 uni-app。它把 Vue 的单文件组件语法(template、script、style)映射成了小程序原生组件,理论上你的 Vue 代码经过编译就能跑到微信小程序里。
3.1 生命周期差异:别用 Vue 的思维硬套小程序
这是第一次从 Web 转小程序的人最容易踩的坑。Vue 页面里最常见的 mounted,在微信小程序对应的通常是 onLoad 或 onShow。uni-app 为了兼容 Vue 写法,提供了一套跨端生命周期,同时也在 H5 端保持了 mounted 的习惯。
我的建议是,小程序中请求页面数据尽量放在 onLoad 里,因为这里能拿到页面参数;而返回页面刷新数据、更新状态则使用 onShow。比如在小程序里从“社团详情页”返回“社团列表页”,列表页如果没有在 onShow 重新请求一次,用户看到的社团简介就可能是旧的,体验很不好。
javascript复制export default {
onLoad(options) {
this.clubId = options.id;
this.fetchClubDetail();
},
onShow() {
// 从审核页面返回后刷新申请状态
if (this.clubId) {
this.fetchMyApplication();
}
},
methods: {
async fetchClubDetail() {
// 调用接口
},
},
};
3.2 页面结构与权限路由处理
我在小程序端做了一个类似 Web 端路由守卫的处理,写在 uni-app 的拦截器或者每个页面 onShow 启动判断里。如果当前用户还没有登录,就跳转到登录页;如果是社团负责人但尝试访问管理员管理面板,就直接拦截。
比起在每个接口处做重复判断,我更倾向于前端只做“按钮显隐”和“页面入口判断”,真正的安全校验全部放在后端接口层。前端判断只是为了更好的交互体验,不能替代后端的权限判断。
页面方面我大致分成了四个 tab:
- 首页:展示社团列表、轮播公告。
- 活动:活动日历与报名入口。
- 我的:个人主页,包含我加入的社团、我的申请记录。
- 管理(有权限才显示):审核学生申请,发布活动与动态。
tab 栏使用原生配置,不带自定义样式,这样组件稳定性高,启动速度也不会被复杂渲染拖慢。
3.3 社团动态列表和 m3u8 视频的播放处理
热搜词里 “vue播放m3u8” 是很多人会在实际项目中遇到的需求。如果只是 Web 端的 vue-video-player,播放 m3u8 相对容易;但到了微信小程序里,原生 video 组件的 src 直接指向 m3u8,部分低版本 iOS 或某些安卓机型容易出现只转圈不播放的现象。
我最后采用的做法是把 HLS 流地址交给后端转一次,由后端返回适配不同终端的播放地址;如果必须直接播放 m3u8,小程序端很多项目也会引入西瓜播放器或腾讯视频插件来解决。下面这段是纯前端用 <video> 加载 mp4 的常规思路,如果你的视频是 mp4 可以直接用;m3u8 就参考上面的处理方案,在后台存储时统一转成 mp4 或者用插件兼容。
html复制<template>
<view class="video-box">
<video
:src="currentVideoUrl"
:controls="true"
:autoplay="false"
object-fit="contain"
@error="onVideoError"
/>
</view>
</template>
不要在前端把非标准 HLS 源直接塞给用户,提前在管理端做视频格式说明与转码提示,比用户在手机上看不到画面再来反馈要舒服得多。
3.4 iOS 与安卓的兼容问题:音频缓存路径、头部导航
我在项目里还遇到两个和兼容相关的点,趁早说一下:
- 音频缓存路径:有些社团动态会附带语音介绍,如果直接把网络地址丢给
<audio>组件,安卓端表现还行,iOS 上可能有跨域加载失败问题。做的时候后端需要返回可访问的 HTTPS 地址,不建议用 HTTP 明文地址,小程序生产环境强制要求 HTTPS,否则苹果端很容易白屏。 - 小程序头部标题:微信小程序顶部导航栏默认来自页面的
navigationBarTitleText。如果希望每个页面都能动态设置标题,留意它不能直接在 Vue 组件里用 CSS 改,必须通过uni.setNavigationBarTitle({ title: xxx })调用原生方法,这个经常被忽略。
4. 数据库设计中的坑:业务边界的另一层
后台功能再花哨,数据库设计跟不上,系统改起来就是泥潭。具体到社团管理系统,我把表拆成用户表、社团表、成员关系表、活动表、申请审批表、动态内容表等。
4.1 用户与社团:为什么不要直接在 user 表里放 club_id
考虑到社团管理场景,最自然的思路是给用户加一个 clubId 字段,标明这个人属于哪个社团。这种设计在“一人一社团”时是不错的,但校园实际情况是一个学生可能同时加入多个社团,甚至某个社长本身也是另一个社团的普通成员。所以我把“用户—社团”的从属关系抽到了单独的成员关系表里:
text复制club_members
- id
- user_id
- club_id
- role: member / leader
- status: active / disabled
- join_time
这样做的好处显而易见:查询“我加入了哪些社团”,只需要一条 WHERE user_id = ?;查询“某个社团有哪些成员”直接按 club_id 扫成员表即可。如果你把社团字段直接塞在 user 表,后面扩展成多社团场景时就要写各种逗号分隔的奇技淫巧。
4.2 审批状态机:申请、通过、拒绝、撤销
入社申请和活动报名都会存在审批状态流转。我喜欢把状态字段设为 pending / approved / rejected / canceled 四种。对应到小型系统里,状态就是一个字符串,不需要建什么状态机引擎,但需要保证后端只允许合法流转方向。例如:
- 学生提交申请:
pending - 负责人或管理员通过:
approved - 负责人或管理员拒绝:
rejected - 学生自己取消:
canceled
approved 的状态不应该能被社团执行“直接改回 pending”,我代码里写接口时会做条件判断:如果要更新的状态不在允许变更范围内,直接返回 40004。
4.3 中学生的敏感信息保护
因为是面向中学生的系统,合规和安全意识要比普通小工具更强。像手机号、学生证号这类个人数据,收集原则必须是“按需收集”:报名参加活动如果不需要手机号联系,就不要展示必填项。后端数据表里我给敏感字段单独做了加密存储,只允许在受控的后台列表里展示脱敏后的手机号(比如 138****1234),普通接口一律不返回完整手机号。细节不多,但值得任何做校园系统的开发者注意。
5. 实操中防不胜防的细节问题与修复
很多从零搭建 Node.js + Vue + 小程序项目的人其实不是栽在业务逻辑上,而是被环境问题、脚本问题、联调问题缠了大半天。
5.1 Windows 下 npm 无法加载脚本
这个问题在热搜词里反复出现:
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本
解决办法分两步:
- 以管理员身份打开 PowerShell,执行:
powershell复制Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
- 这样 npm.ps1 才有执行权限。如果你希望更彻底一点,也可以直接在命令行工具里禁用 PowerShell 脚本执行,改用 cmd 去敲 npm 命令,但治标不治本,不建议。
设置属性时会有确认提示,输入 Y 回车即可。我一般习惯在项目里再用一个 .npmrc 文件锁定镜像源:
text复制registry=https://registry.npmmirror.com
国内用户安装依赖可以快不少,同时团队其他人拉项目后也统一走同一镜像。
5.2 微信开发者工具中打开项目提示“不是开发者”
如果你用 HBuilderX 或 uni-app 编译出的微信小程序项目路径,在微信开发者工具里导入时提示无法识别或不是开发者项目,通常是因为没有指定 project.config.json 文件。用 HBuilderX 开发的项目,要先在 manifest 里配置好小程序 AppID,菜单里执行“运行到小程序模拟器”,让它自动生成编译产物和项目配置,而不是手动去选源码目录。如果非要手动导入,项目根目录应该存在由 HBuilderX 生成的 unpackage/dist/dev/mp-weixin 目录,而不是 src 目录。
5.3 小程序跳转另一个小程序的配置
小程序中实现 A 跳 B,不能简单用 uni.navigateTo,因为小程序不允许直接跳转到任意页面。开通条件是:两个小程序必须关联在同一个微信开放平台账号下,或者至少属于同一主体,然后在 A 小程序的 app.json 里配置 navigateToMiniProgramAppIdList,把 B 小程序的 AppID 加进去。有时在配置分包路径后跳转仍然不生效,原因很可能是目标跳转页面写错了路径,或者 B 小程序没有通过分包的页面路径配置在跳转链接中。这是我在热词里看到很多人都卡住的问题,这里做一个集中说明:跳转小程序时,path 参数必须完整,例如 pages/index/index;分包页则需要带分包名,例如 packageA/pages/detail/detail。
5.4 管理端页面调试的跨域问题
微信小程序端是没有浏览器同源策略的,但 Vue 管理端开发时必定遇到跨域。我在 .env.development 里配置了代理:
env复制VITE_BASE_API=/api
然后在 vite.config.js 中配置 proxy:
javascript复制server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
}
这样管理端写代码时请求的是 /api/auth/login,实际后端收到的就是 /auth/login。生产环境再换成真实的 HTTPS API 域名。小程序端则直接把 BASE_URL 指向后端线上地址,不需要走 proxy。
5.5 小程序端表单与图片上传:压缩组件不生效的坑
活动中经常要上传图片,学生手机相册里的照片动不动几 MB,直接上传既慢又浪费云存储。热搜词里“compressor 小程序原生”在这个项目里我用过一次小程序的图片压缩能力,或者通过 uni.compressImage 来实现:
javascript复制uni.chooseImage({
count: 1,
success: (res) => {
const tempFilePath = res.tempFilePaths[0];
uni.compressImage({
src: tempFilePath,
quality: 70,
success: (compressRes) => {
// 这里拿 compressRes.tempFilePath 去上传
this.uploadFile(compressRes.tempFilePath);
},
});
},
});
在 iOS 上压缩后的图片偶尔还带着一张大到离谱的原图,是因为你没检查压缩结果的文件大小。建议在压缩成功后,再获取一次文件信息:
javascript复制uni.getFileSystemManager().getFileInfo({
filePath: compressedPath,
success: (info) => console.log('压缩后大小KB:', info.size / 1024),
});
超过 500KB 就直接弹提示给用户重新选择。
6. 从 0 到部署的整体时间线复盘
如果把整个项目排期看一遍,大概会更直观:
- 需求梳理与原型图:1-2 天,产出角色清单和页面草图。
- Node.js 后端:3-4 天,重点完成登录鉴权、社团/活动/申请的 CRUD、文件上传。
- Vue 管理端:3-4 天,管理端只需要做信息列表、审核流程、数据统计。
- 小程序端:5-6 天,核心页面是社团展示、活动报名和申请记录。
- 联调与真机测试:2-3 天,重点排查 iOS 播放与上传图片、审批推送提醒。
- 部署上线:1-2 天,后端部署在云服务器(Nginx 反向代理 + PM2 守护进程),小程序上传审核。
踩过的坑说多不多,说少不少,但这些经验积累下来,对后面看类似项目的代码和架构会有很大帮助。后来再接手同类型系统,我一般先打开它的登录鉴权中间件和申请状态字段,就能大概猜出这个系统是认真设计过的,还是为了赶工把业务全写在控制器里。数据表的关联、状态机的流转逻辑、统一返回体和错误码,这些基础设计看起来不炫,但才是支撑一个项目长期不腐烂的骨架。
如果你正准备用 Node.js + Vue 做小程序项目,我的建议是直接找一个真实场景的闭环去练手,不要只盯着某个单一技术点。小程序端长什么样、管理后台长什么样、后端 API 要不要严格校验,这些都是开发过程中才体会得到的取舍。
