基于MERN栈(Node.js)的成绩管理系统:短信方案及完整实现问询
成绩管理系统实现方案(MERN栈)
一、短信服务方案(满足2小时发送500+条需求)
免费方案
- 阿里云短信试用:新用户提供100-500条免费额度,国内覆盖全,API调用简单,适合初期测试。需完成个人/企业资质认证,免费额度耗尽后需切换付费模式。
- 腾讯云短信试用:新用户免费额度可覆盖500条需求,支持批量发送,接口文档清晰,测试阶段完全够用。
付费方案
- 阿里云短信:批量发送单价低至0.035元/条,单账号默认每秒可发10条(可申请提额),2小时发送500条无压力。支持模板化发送,长期使用成本可控。
- 腾讯云短信:阶梯定价,成本与阿里云接近,短信签名、模板审核速度快,接口集成难度低,适配MERN栈技术栈。
- 容联云通讯:针对教育行业有专属套餐,批量发送效率高,支持定时发送,适合成绩通知这类场景,配套客服支持完善。
二、MERN栈系统完整实现方案
1. 系统架构
- 前端(React):教师成绩录入页、管理员审核页、基础数据展示组件
- 后端(Node.js+Express):接口服务、短信触发逻辑、角色权限控制
- 数据库(MongoDB):存储用户信息、成绩数据、短信发送日志
2. 数据库集合设计
users集合
{ _id: ObjectId, username: String, // 登录账号 password: String, // bcrypt加密后的密码 role: String, // teacher/admin/parent phone: String, // 家长手机号 studentId: String // 关联学生ID(仅家长用户) }
scores集合
{ _id: ObjectId, studentId: String, studentName: String, subject: String, score: Number, teacherId: String, // 录入教师ID status: String, // pending/approved/rejected createTime: Date, updateTime: Date }
smsLogs集合
{ _id: ObjectId, studentId: String, parentPhone: String, content: String, sendStatus: String, // success/failed sendTime: Date }
3. 后端实现(Node.js+Express)
核心依赖安装
npm install express mongoose bcryptjs jsonwebtoken axios dotenv
权限控制中间件
const authMiddleware = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ msg: '无权限访问' }); try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (err) { res.status(401).json({ msg: 'Token无效' }); } }; const requireRole = (role) => (req, res, next) => { if (req.user.role !== role) return res.status(403).json({ msg: '权限不足' }); next(); };
教师成绩录入接口
router.post('/scores', authMiddleware, requireRole('teacher'), async (req, res) => { try { const { studentId, studentName, subject, score } = req.body; const newScore = new Score({ studentId, studentName, subject, score, teacherId: req.user.id, status: 'pending', createTime: new Date() }); await newScore.save(); res.status(201).json(newScore); } catch (err) { res.status(500).json({ msg: '录入失败' }); } });
管理员审核与短信触发接口
router.put('/scores/:id', authMiddleware, requireRole('admin'), async (req, res) => { try { const { status } = req.body; const score = await Score.findById(req.params.id); if (!score) return res.status(404).json({ msg: '成绩记录不存在' }); score.status = status; score.updateTime = new Date(); await score.save(); // 审核通过后发送短信 if (status === 'approved') { const parent = await User.findOne({ studentId: score.studentId, role: 'parent' }); if (parent?.phone) { const smsContent = `【学校通知】您的孩子${score.studentName}的${score.subject}成绩为${score.score}分,请查收。`; await sendSms(parent.phone, smsContent); // 记录发送日志 const smsLog = new SmsLog({ studentId: score.studentId, parentPhone: parent.phone, content: smsContent, sendStatus: 'success', sendTime: new Date() }); await smsLog.save(); } } res.json(score); } catch (err) { res.status(500).json({ msg: '审核失败' }); } });
短信发送通用函数(以阿里云为例)
const axios = require('axios'); const crypto = require('crypto'); const sendSms = async (phone, content) => { const accessKeyId = process.env.ALIYUN_ACCESS_KEY; const accessKeySecret = process.env.ALIYUN_SECRET_KEY; const params = { PhoneNumbers: phone, SignName: '你的短信签名', TemplateCode: '你的短信模板ID', TemplateParam: JSON.stringify({ scoreContent: content }) }; // 阿里云短信API签名逻辑(参考官方文档实现) // 生成签名后发起请求 const result = await axios.post('https://dysmsapi.aliyuncs.com/', params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); return result.data; };
4. 前端实现(React)
核心依赖安装
npm install react-router-dom axios react-hook-form
教师成绩录入组件
import { useForm } from 'react-hook-form'; import axios from 'axios'; const ScoreInput = () => { const { register, handleSubmit, formState: { errors } } = useForm(); const onSubmit = async (data) => { try { const token = localStorage.getItem('token'); await axios.post('/api/scores', data, { headers: { Authorization: `Bearer ${token}` } }); alert('成绩录入成功'); } catch (err) { alert('录入失败'); } }; return ( <form onSubmit={handleSubmit(onSubmit)} className="score-form"> <input {...register('studentId', { required: true })} placeholder="学生ID" /> {errors.studentId && <span>学生ID为必填项</span>} <input {...register('studentName', { required: true })} placeholder="学生姓名" /> {errors.studentName && <span>学生姓名为必填项</span>} <input {...register('subject', { required: true })} placeholder="科目" /> {errors.subject && <span>科目为必填项</span>} <input type="number" {...register('score', { required: true, min: 0, max: 100 })} placeholder="成绩" /> {errors.score && <span>请输入0-100之间的成绩</span>} <button type="submit">提交成绩</button> </form> ); }; export default ScoreInput;
管理员审核组件
import { useEffect, useState } from 'react'; import axios from 'axios'; const ScoreReview = () => { const [pendingScores, setPendingScores] = useState([]); useEffect(() => { const fetchPendingScores = async () => { const token = localStorage.getItem('token'); const res = await axios.get('/api/scores?status=pending', { headers: { Authorization: `Bearer ${token}` } }); setPendingScores(res.data); }; fetchPendingScores(); }, []); const handleReview = async (scoreId, status) => { try { const token = localStorage.getItem('token'); await axios.put(`/api/scores/${scoreId}`, { status }, { headers: { Authorization: `Bearer ${token}` } }); setPendingScores(pendingScores.filter(score => score._id !== scoreId)); alert(`已${status === 'approved' ? '通过' : '驳回'}该成绩`); } catch (err) { alert('审核操作失败'); } }; return ( <div className="review-container"> <h2>待审核成绩列表</h2> {pendingScores.length === 0 ? ( <p>暂无待审核成绩</p> ) : ( pendingScores.map(score => ( <div key={score._id} className="score-item"> <p>学生:{score.studentName} | 科目:{score.subject} | 成绩:{score.score}</p> <button onClick={() => handleReview(score._id, 'approved')}>通过</button> <button onClick={() => handleReview(score._id, 'rejected')}>驳回</button> </div> )) )} </div> ); }; export default ScoreReview;
5. 部署建议
- 前端:部署到Vercel/Netlify,免费额度足够支撑小型系统访问
- 后端:部署到Render/Heroku,免费版可满足基础接口服务需求,环境变量存储密钥信息
- 数据库:使用MongoDB Atlas免费集群,提供512MB存储,完全适配系统数据量
内容的提问来源于stack exchange,提问作者Coding Acumen
相关产品推荐
相关产品推荐

