You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 17:25:09