每年迎新季,学工处的老师都要面对几百上千条新生数据的录入、核对、汇总,辅导员在群里一遍遍催大家填表,新生到校后还要排长队交材料、验身份、领宿舍钥匙。我去年帮一所高职院校做了这套新生入学报道系统,用 Django + Python 微信小程序 搭了一套预报到注册方案,把原来需要两天才能办完的线下流程压缩到了“在家填一次表、到校扫一下码”。这篇文章我会把整个系统从需求拆解、建模、接口设计、小程序前端到部署上线的完整思路写出来,适合正在做毕业设计、或者学校信息中心想自建这套系统的朋友参考。
先说清楚这套系统到底做什么:新生收到录取通知书后,通过微信小程序完成个人信息补全、预报到登记、到校方式填报、缴费状态查询,生成一个预报道二维码;到校当天,老师或志愿者扫码核验,系统自动完成注册确认和宿舍分配。后端用 Django 提供 RESTful API,小程序负责采集和展示,数据库采用 MySQL。技术上不复杂,但要把“流程闭环”想清楚,否则很容易做成一个中看不中用的表单工具。
1. 先把业务边界画清楚,再决定系统要建多胖
1.1 新生预报到场景里的核心痛点
做这套系统之前,我调研了学校原来的报到流程。新生到校后要跑四个窗口:资格审查、缴费确认、宿舍分配、材料归档。每个窗口都要人工核对身份证、录取通知书、准考证,信息重复录入不说,高峰期排队时间基本在一小时以上。而且辅导员在开学前根本拿不到准确的学生到校时间,接站车辆安排全靠拍脑袋。
预报到系统的核心价值,就是把“必须到校才能做的事”和“在家就能做的事”分开。身份核验和纸质材料归档必须线下做,但信息采集、缴费状态同步、到校时间登记、宿舍偏好(比如是否申请四人寝)、军训服尺码统计,这些完全可以前置到线上。这样一来,迎新当天只需要做一件事:扫码确认身份,然后直接领钥匙入住。
这套思路听起来很直白,但很多同类系统的失败之处,恰恰是想把线下流程也强行搬到线上,结果既增加了老师的工作量,又没解决排队问题。所以我在设计之初就定了一个硬性边界:线上的归线上,线下的归线下,系统不做流程替代,只做流程前置。
1.2 功能模块盘点:学生端、教师端、管理后台
整个系统按用户角色拆成三个端,权限边界非常清晰。
学生端(微信小程序):
- 微信授权登录,绑定本人学号与身份证号
- 个人信息补全:紧急联系人、家庭住址、是否建档立卡户、军训服尺码
- 预报到登记:到达日期、到达站点、是否需要接站、陪同人数
- 缴费信息查看:学费、住宿费、代收费是否已缴纳
- 报到凭证:生成带状态的二维码,绿色代表信息完整可报到
教师端(小程序或后台):
- 扫码核验学生报到凭证
- 查看当日到校统计、各时段到校人数
- 一键导出未预报到学生名单
管理后台(Web):
- 导入录取数据,批量初始化学生账号
- 维护院系、专业、班级、宿舍楼栋数据
- 管理通知公告发布
- 查看迎新大屏数据面板
这里稍微提醒一句,有些学校会把教师端也做成小程序,但我建议教师端优先用 Web 管理后台,因为扫码核验需要摄像头,Web 端调用摄像头比小程序端稳得多,尤其是 Windows 系统下的小程序开发者工具和真机兼容性问题,折腾起来非常烦。
1.3 为什么选 Django 而不是 Spring Boot 或 Node.js
选型这件事,被问过太多次了。说实话,这套系统用 Node.js 或者 Java 都能做,但 Django 在这类场景里有几个别人比不了的优势。
首先是自带 Admin 后台。新生报到这种系统,业务逻辑不算复杂,但数据维护量很大——院系、专业、班级、学生录取信息、宿舍分配,这些数据如果每个都写 CRUD 页面,开发量直接翻倍。Django Admin 稍微配置一下,信息中心的老师自己就能维护基础数据,省去不少定制开发时间。
其次是 DRF(Django REST Framework) 做小程序接口非常顺手。序列化器、视图集、权限控制都是现成的,配合 JWT 认证,小程序的登录态管理基本不用自己造轮子。
第三是 Python 生态对数据处理友好。迎新结束后,学工处经常要导各种维度的统计报表——省外生源比例、各时段到校人数、未报到学生名单——用 pandas 直接跑一遍 DataFrame 比在 Java 里拼 SQL 舒服太多。
当然,如果学校的并发量真的大到同一秒几千人抢着提交表单,那 Spring Boot 的线程模型会更有优势。但高校新生报到这个场景,一个批次最多也就几千人,而且预报到时间窗口长达两周,分布式压力几乎可以忽略。Django 的性能瓶颈不在这套系统的关键路径上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据建模和接口设计:先想清楚状态流转,再写代码
2.1 核心表结构和字段设计
数据库建模是这套系统的灵魂,字段设计得好不好,直接决定了后面写业务逻辑时是丝滑还是想骂人。我先贴出核心的几张表,然后逐个解释设计理由。
python复制# models.py 核心模型示意
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
# 扩展 Django 默认用户模型,避免单独建 profile 表
USER_TYPE_CHOICES = (
('student', '学生'),
('teacher', '教师/管理员'),
)
user_type = models.CharField(max_length=10, choices=USER_TYPE_CHOICES)
openid = models.CharField(max_length=64, unique=True, null=True, blank=True)
session_key = models.CharField(max_length=64, null=True, blank=True)
phone = models.CharField(max_length=11, blank=True)
class Student(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='student_profile')
student_no = models.CharField(max_length=20, unique=True, db_index=True, verbose_name='学号')
name = models.CharField(max_length=50, verbose_name='姓名')
id_card = models.CharField(max_length=18, verbose_name='身份证号')
gender = models.CharField(max_length=4, choices=(('male','男'),('female','女')))
college = models.ForeignKey('College', on_delete=models.PROTECT, verbose_name='院系')
major = models.ForeignKey('Major', on_delete=models.PROTECT, verbose_name='专业')
class_name = models.CharField(max_length=50, verbose_name='班级')
enrollment_year = models.IntegerField(default=2025, verbose_name='入学年份')
is_filed_card = models.BooleanField(default=False, verbose_name='是否建档立卡户')
address = models.CharField(max_length=200, blank=True, verbose_name='家庭地址')
emergency_contact = models.CharField(max_length=50, blank=True, verbose_name='紧急联系人')
emergency_phone = models.CharField(max_length=11, blank=True, verbose_name='紧急联系电话')
class PreReport(models.Model):
"""预报到记录"""
student = models.OneToOneField(Student, on_delete=models.CASCADE, related_name='pre_report')
STATUS_CHOICES = (
('draft', '未提交'),
('submitted', '已提交'),
('verified', '已核验'),
('checked_in', '已完成报到'),
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', db_index=True)
arrive_date = models.DateField(null=True, blank=True, verbose_name='到达日期')
arrive_time = models.TimeField(null=True, blank=True, verbose_name='到达时段')
arrive_station = models.CharField(max_length=100, blank=True, verbose_name='到达站点')
is_pickup = models.BooleanField(default=False, verbose_name='是否需要接站')
companion_num = models.IntegerField(default=0, verbose_name='陪同人数')
luggage_num = models.IntegerField(default=0, verbose_name='行李件数')
uniform_size = models.CharField(max_length=10, blank=True, verbose_name='军训服尺码')
checkin_time = models.DateTimeField(null=True, blank=True, verbose_name='现场核验时间')
checkin_operator = models.ForeignKey(User, null=True, on_delete=models.SET_NULL, verbose_name='核验人')
class DormitoryAssignment(models.Model):
"""宿舍分配"""
student = models.OneToOneField(Student, on_delete=models.CASCADE)
building = models.CharField(max_length=50, verbose_name='楼栋')
room_no = models.CharField(max_length=20, verbose_name='房间号')
bed_no = models.CharField(max_length=10, verbose_name='床号')
assigned_at = models.DateTimeField(auto_now_add=True)
有几个字段设计决策值得展开说。
第一,Student 和 User 用 OneToOne 关联而不是合并成一张表。因为小程序登录依赖的 User 表里存 openid、session_key 这些微信体系的数据,而学生的学籍信息是学校体系的数据,两者生命周期和字段来源完全不同。合并成一张表虽然查询方便,但一旦系统以后要对接统一身份认证,扩展性就差了。
第二,PreReport 采用状态机而非简单的布尔字段。很多人会把预报到做成一个 is_submitted 字段,但实际业务里状态不止“提交了”和“没提交”两个状态:学生提交后辅导员可能退回补充材料,到校后现场核验又是另一个状态,核验完之后宿舍分配和注册确认还要更新状态。我用 draft → submitted → verified → checked_in 四个状态来串整个流程,每个状态转换都对应一个前端操作节点,这样排查问题时能清楚的知道某个学生卡在了哪个环节。
第三,身份证号明文存储但要做权限控制。身份证属于敏感个人信息,系统里一定要做脱敏和权限控制。展示时默认只显示前六位和后四位,导出文件时也要做记录。我见过有系统直接把整个表导出丢到学工群里,这是非常危险的。
2.2 接口设计:统一返回体、JWT 认证、幂等提交
接口设计遵循了 RESTful 风格,但是加了统一的返回包装:
python复制# utils/response.py
def ok(data=None, message='success'):
return JsonResponse({'code': 0, 'message': message, 'data': data})
def fail(code=1, message='error'):
return JsonResponse({'code': code, 'message': message, 'data': None})
为什么不直接用 DRF 默认的 Response?因为我们小程序的网络层封装了一个统一的响应拦截器,不管哪个接口返回的都是 {code, message, data} 三层结构,前端只需要判断 code 是否为 0,就能统一处理错误提示,不需要每个接口单独写 catch。这个约定在联调阶段非常省心。
登录接口是这套系统的第一道关卡。小程序的 wx.login() 只能拿到临时 code,后端需要拿 code 去微信的 jscode2session 接口换 openid 和 session_key:
python复制# views/auth.py
import requests, time
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
class WxLoginView(APIView):
permission_classes = [AllowAny]
def post(self, request):
code = request.data.get('code')
appid = settings.WX_APPID
secret = settings.WX_SECRET
url = 'https://api.weixin.qq.com/sns/jscode2session'
resp = requests.get(url, params={
'appid': appid,
'secret': secret,
'js_code': code,
'grant_type': 'authorization_code'
})
data = resp.json()
if 'openid' not in data:
return fail(code=1001, message='微信登录失败,请重试')
openid = data['openid']
# 先用 openid 找用户,找不到则创建
user, created = User.objects.get_or_create(openid=openid, defaults={
'username': f'wx_{openid[-8:]}',
'user_type': 'student',
})
# 签发 JWT
token = jwt_encode(user)
return ok({'token': token, 'user_type': user.user_type})
这里有个实际开发中容易踩的坑:jscode2session 的 code 只能用一次,而且有效期只有五分钟。如果后端在处理时网络波动导致微信接口超时,前端重新调用 wx.login 也会拿到全新的 code,所以前端要做到“登录失败后自动重新走一遍 wx.login 再请求,而不是拿旧的 code 死磕”。我在前端封装了一个 login 方法,内部做了两到三次重试,实测稳定很多。
预报到表单提交接口还有一个幂等性问题。新生填完一大张表,点击提交,如果网络抖动导致请求失败,前端通常会重试,这时同一个请求可能被后端处理两次。虽然 PreReport 表用了 OneToOne 关联,第二次插入会报 UniqueConstraint 错误,但这样用户看到的就是一个莫名其妙的 500 错误,体验不好。
更好的做法是前端生成一个 client_token(UUID),提交时带上,后端在事务里先检查这个 token 是否已经处理过;如果是,直接返回上次成功的结果。代码逻辑如下:
python复制class PreReportView(APIView):
def post(self, request):
client_token = request.data.get('client_token')
# 从缓存或者专门表里查是否已存在相同 token
if cache.get(f'pre_report_{client_token}'):
data = cache.get(f'pre_report_{client_token}')
return ok(data)
with transaction.atomic():
pre_report, created = PreReport.objects.update_or_create(
student=request.user.student_profile,
defaults={...}
)
cache.set(f'pre_report_{client_token}', serializer.data, 60*60)
return ok(serializer.data)
2.3 数据权限:学生只能操作自己的数据
Django REST Framework 自带的 permissions.IsAuthenticated 只能校验用户是否登录,不能校验数据归属。例如学生 A 登录后,如果直接调 /api/student/2/ 的 GET 接口,后端不应该返回学生 B 的个人信息。
我建议重写 get_queryset 方法,强制在学生视图里做归属过滤:
python复制class StudentProfileView(RetrieveAPIView):
serializer_class = StudentSerializer
def get_queryset(self):
# 学生只能看到自己的档案,教师可以看到全量
if self.request.user.user_type == 'teacher':
return Student.objects.all()
return Student.objects.filter(user=self.request.user)
这层校验不啰嗦,但非常重要。前端不管怎么控制按钮显隐都只是体验层面,真正的数据安全防线必须写在接口层。
3. 小程序端:从授权登录到预报到流程闭环
3.1 微信登录与绑定学号:两段式设计
小程序端拿到 code 换 token 后,用户还只是“微信用户”,并没有和真实学籍关联。这里我做了两段式登录:
第一段是静默登录。用户打开小程序,自动调 wx.login 换 token,拿到一个待绑定的状态。此时首页可以展示学校宣传内容,但在点“预报到”按钮时会跳到绑定页,要求输入学号和身份证号。
第二段是学号绑定。前端把学号、身份证号(可以只输后六位)传回后端,后端在 Student 表里查这两项是否匹配,匹配则把该学生的 User 关联绑定。这个设计比“微信一键获取手机号再绑定”稳妥得多——因为学号+身份证号本身就是学校体系里最强的实名凭证,而且不需要额外申请微信的资质接口。
javascript复制// pages/login/login.js
async function loginWithStudentNo(studentNo, idCardTail) {
const loginCode = await wxLoginWithRetry();
const res = await request('/api/auth/bind/', {
method: 'POST',
data: {
code: loginCode,
student_no: studentNo,
id_card_tail: idCardTail,
}
});
if (res.code === 0) {
wx.setStorageSync('token', res.data.token);
wx.redirectTo({ url: '/pages/index/index' });
} else {
wx.showToast({ title: res.message, icon: 'none' });
}
}
3.2 首页信息聚合:给新生一个清晰的任务清单
预报到小程序最重要的不是页面多漂亮,而是新生打开后能一眼看明白“我该做什么、做到哪一步了”。
我做了三块展示:
- 头部状态卡:显示“已完成预报到登记”“待补充材料”“未提交”等状态,用颜色区分,一眼就能看到自己的进度。
- 流程清单:按新生视角把全部流程拆成“绑定学号、完善信息、预报到登记、查看凭证”四步,已经完成的打勾,当前可点的高亮,未解锁的置灰。
- 重要通知:顶部滚动公告栏,展示学校发布的迎新通知,比如接站安排、防诈骗提醒。
首页接口需要聚合多个表的数据,我在后端用序列化器嵌套一次性返回,避免小程序端频繁发请求:
python复制class HomeDashboardSerializer(serializers.ModelSerializer):
pre_report = PreReportSerializer(read_only=True)
profile_status = serializers.SerializerMethodField()
class Meta:
model = Student
fields = ['id', 'name', 'student_no', 'college_name', 'profile_status', 'pre_report']
3.3 动态表单:一个配置驱动的信息采集方案
预报到信息采集的字段会随院系变化而变化:有的院系需要登记电脑品牌型号(方便安排机房座位),有的需要登记是否自带被褥。这种需求如果写死表单组件,后期维护成本极高。
我采用了配置驱动的动态表单:后端在 StudentProfile 和 PreReport 表之外,再建一张 extra_field_config 表,存字段名、类型(input/select/upload)、是否必填、选项列表。小程序端拿到配置后动态渲染表单。
json复制[
{"key": "uniform_size", "label": "军训服尺码", "type": "picker", "required": true, "options": ["S","M","L","XL","XXL"]},
{"key": "is_bring_bedding", "label": "是否自带被褥", "type": "switch", "required": false},
{"key": "arrive_station", "label": "到达站点", "type": "picker", "required": true, "options": ["火车站","高铁站","飞机场","汽车站"]}
]
前端遍历数组,根据 type 渲染对应组件,提交时把所有字段打包成 JSON 存到 extra_data 字段里。这样后续要加字段,管理员在后台添加配置即可,不需要发版小程序。
3.4 照片采集与证件审核:头像上传的设计细节点
腾讯这几年调整了小程序获取用户头像的策略——wx.getUserProfile 已经不能直接返回高清头像了,微信鼓励用户主动上传头像。在新生报到场景里,学校其实更需要的是新生上传一张用于制作校园卡的证件照,而不是微信头像。
我们让新生在小程序里调用 wx.chooseMedia 拍摄或选择照片,前端用 canvas 裁剪成 1:1 尺寸,再通过 wx.uploadFile 上传到后端。上传时有几个细节:
- 限制文件大小在 2MB 以内,前端用
wx.compressImage压缩后再传; - 后端用 PIL 验证图片是否损坏,同时检查宽高比例;
- 文件名用学号命名,避免乱码文件名导致后续归档困难;
- 上传成功后把 URL 存到 Student 表的 avatar 字段,生成校园卡时直接拉取这个字段。
3.5 审核状态与消息触达:轮询还是订阅消息
预报到信息提交后,辅导员需要后台审核,比如异常的高消费专业申请材料、填报信息不完整等。审核通过或驳回后,小程序需要通知到学生。
最省事的方式是首页的“流程清单”状态卡通过轮询更新,学生每次进小程序拉一次最新状态就行。缺点是学生如果不主动打开小程序,就收不到审核结果。
更好的是用微信订阅消息。后端在审核状态变化时,调用 subscribeMessage.send 给学生推送审核结果。这个方案需要学生在提交表单时主动订阅一次性订阅消息,然后每次推送消耗一条授权。实际使用中,订阅消息的唤醒率确实比短信高,而且免费。
python复制# services/wx_message.py
def send_report_result(openid, result_content):
access_token = get_access_token()
url = f'https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={access_token}'
data = {
'touser': openid,
'template_id': settings.WX_REPORT_RESULT_TEMPLATE_ID,
'page': 'pages/detail/detail',
'data': {
'thing1': {'value': result_content[:20]},
'time2': {'value': current_time_str()},
'thing3': {'value': '请在小程序内查看详情'}
}
}
requests.post(url, json=data)
需要注意,订阅消息模板中的字段类型是有限制的,比如 thing 类型只能存 20 个字以内的文本,超出会被微信拒绝。所以推送内容一定要精简,详细的审核意见让用户点击卡片跳进小程序查看。
3.6 小程序常见的登录失败问题排查
标题相关热搜里有一条“小程序获取登录后的微信用户失败:wx1cb4398e1413dce7”,这是个非常经典的错误提示。实际遇到这个报错,十有八九不是后端的问题,而是 wx.login 拿到的 code 被用于换取了用户信息而不是 openid。换句话说,前端误用了 wx.getUserProfile 或者 wx.getUserInfo 的返回值,而微信在小程序基础库 2.10.4 版本之后,这些接口都会走用户隐私授权弹窗,如果用户不点允许,请求就返回失败。
解决方式是明确区分:
wx.login()获取的是登录凭证 code,只能用于后端换 openid;wx.getUserProfile()获取的是用户资料信息,需要用户手动点授权弹窗,而且只能获取头像和昵称;- 新版小程序已经不建议用
wx.getUserProfile获取头像,而是使用<button open-type="chooseAvatar">让用户主动选择头像。
所以如果你遇到获取用户失败,先检查是不是把 wx.login 和 wx.getUserProfile 混在一起用了。换成两段式设计后,这个问题就彻底消失了。
4. 部署上线与踩坑记录:宝塔、HTTPS、并发与安全
4.1 服务器部署:宝塔面板 + uWSGI + Nginx
部署方案我用了宝塔面板。虽然有些人对宝塔有偏见,但在学校这个环境里,宝塔的可视化管理能省下大量运维沟通成本——信息中心的老师不一定懂 Nginx 配置文件,但宝塔的界面他们很快能上手。
部署步骤大致如下:
- 在 Linux 服务器安装 Python 3.9+,用
pip安装django、djangorestframework、djangorestframework-simplejwt、mysqlclient、uwsgi。 - 在宝塔中新建 MySQL 数据库,创建专门账号(不要用 root),把 Django 的
ALLOWED_HOSTS配置成域名或服务器 IP。 - 使用 uWSGI 启动 Django:
ini复制[uwsgi]
http-timeout = 600
chdir = /www/wwwroot/checkin/
module = checkin.wsgi:application
master = true
processes = 4
threads = 2
socket = 127.0.0.1:8000
vacuum = true
die-on-term = true
- 在宝塔中配置 Nginx 反向代理,将 80/443 端口转发到 uWSGI 的 socket 地址。
- 申请 SSL 证书并强制 HTTPS。这个必须做,微信小程序要求所有 request 请求必须是 HTTPS,而且证书链要完整,不能是自签名证书。
有个部署层面的坑非常典型:Django 的 DEBUG 忘记关闭。DEBUG = True 时,Django 会把所有错误堆栈打印到页面上,直接暴露服务器路径和数据库信息,非常危险。上线前必须改成 DEBUG = False,同时配置好 STATIC_ROOT 和 MEDIA_ROOT,用 Nginx 托管静态文件和上传的图片。
4.2 域名白名单与服务器配置:小程序 request 合法域名
小程序原生环境里,wx.request 请求的域名必须在微信公众平台配置为业务域名。这个配置有两件事要做:
- 在公众平台「开发管理」→「服务器域名」里添加
request合法域名,必须是https://前缀。 - 域名需要绑定 SSL 证书并验证所有权。
如果你在小程序开发者工具里能调通接口,但真机上请求失败,90% 是合法域名没配置或者证书过期。开发者工具可以勾选“不校验合法域名”,但真机不行,必须老老实实配好。
另外,小程序对并发请求数有限制,不要在小程序端并发发起超过 10 个请求。首页聚合接口把多表数据一次返回,也是出于减少并发连接的考虑。
4.3 高并发报名场景下的锁处理:开学前三天的那次请求风暴
开学前三天是预报到的高峰期,白天经常出现同一秒多个学生提交表单。如果没有做好并发控制,数据库会产生重复记录、状态错乱等问题。
最常见的坑出现在宿舍分配的分配逻辑。比如某个四人寝室最后只剩一个床位,但两个学生同时提交入住申请,后端如果不加锁,就会出现“超卖”现象,把同一个床位分给两个人。
我用 select_for_update() 解决:
python复制from django.db import transaction
with transaction.atomic():
bed = DormitoryBed.objects.select_for_update().filter(
room=room, is_occupied=False
).first()
if bed is None:
return fail(message='该寝室已满,请选择其他寝室')
bed.is_occupied = True
bed.save()
assignment = DormitoryAssignment.objects.create(student=student, bed=bed)
select_for_update() 会给查询出来的行加排他锁,直到事务结束。这样第二个并发请求会一直等待第一个事务提交,从而避免超卖。这段代码一定要放在 transaction.atomic() 里,否则锁的释放时机无法控制。
类似的问题也出现在“生成报到二维码”上。二维码内容通常是学号加密串,生成过程并不存在竞争条件,但如果你用时间戳做唯一标识,那么同一毫秒内两个请求可能生成相同的值,造成缓存覆盖。我直接在学号后面拼接随机字符串,确保二维码唯一性。
4.4 数据敏感信息处理:脱敏、加密与审计
新生报到系统涉及身份证号、手机号、家庭住址、家长联系方式等敏感数据,上线前必须做好这几件事:
- 接口返回脱敏:身份证号默认只返回前六后四,手机号只返回前后三位。前端如果需要完整字段(例如学生自己核对),可以单独调一个需要二次确认的接口。
- 数据库加密存储:如果学校安全要求较高,身份证号在入库前用 AES 加密,查询时解密。注意密钥不要硬编码在代码仓库里,使用环境变量或专门的密钥管理服务。
- 操作日志审计:对后台导出、查看敏感信息的操作留痕,记录操作人、时间、查看的数据范围。不要等出了事故再查日志,那时候往往是查不到的。
- Excel 导出加水印:学工处经常要导出全班学生联系方式,导出文件加一行水印,内容包括导出人和导出时间,能有效减少文件外传后的追溯难度。
这些点做起来不难,但能体现系统的靠谱程度。学校信息化评审时,“数据安全与隐私保护”往往是一个很加分的板块。
4.5 性能优化:查询量大了该怎么办
预报到期间接口的查询量远高于日常,几个常见的性能瓶颈和优化方法:
- 首页聚合接口:每次请求都要查询 Student、PreReport、DormitoryAssignment、Notification 多张表。如果学生量级到几千,可以在 Django ORM 中使用
select_related和prefetch_related一次性把关联表查出来,避免N+1查询。 - 公告板接口:频繁读取的公告数据可以加 Redis 缓存,设置过期时间为 60 秒,极大减少数据库压力。
- 二维码核验接口:核验是到校瞬间的高频操作,建议用 Redis 直接存二维码内容对应的学生 ID,核验时先查缓存,命中再落到数据库。
python复制from django.core.cache import cache
def get_student_id_by_token(token):
key = f'checkin_token_{token}'
student_id = cache.get(key)
if student_id is None:
student_id = Student.objects.filter(checkin_token=token).values_list('id', flat=True).first()
if student_id:
cache.set(key, student_id, 60 * 60 * 24) # 缓存一天
return student_id
4.6 上线前必须测试的三类场景
我总结的测试清单里,有三类场景是最容易被忽略但最容易翻车的:
- 弱网测试:用开发者工具里的小程序网络面板模拟弱网,确认提交按钮会不会出现重复点击、loading 状态有没有覆盖。尤其在填写一大张表单的场景,弱网重试逻辑没做好,学生容易气到卸载小程序。
- 并发测试:用 JMeter 或 Locust 模拟 500 个并发用户同时提交预报到和申请宿舍,重点观察数据库死锁次数、接口响应耗时、服务器 CPU 占用。
- 权限测试:分别用学生、教师、管理员三种身份访问所有接口,确认无法越权。特别是学生账号尝试调用管理端接口时,必须返回 403。
5. 部署环境搭建与交付避坑
5.1 环境清单:从零到可运行的依赖版本组合
Python 生态的版本问题经常让人崩溃,这里列一套我实测稳定的组合:
| 组件 | 版本 | 备注 |
|---|---|---|
| Python | 3.9.18 | 3.10 也可以,但 3.9 对第三方库兼容性最好 |
| Django | 4.2 | LTS 版本,安全性更新充足 |
| Django REST Framework | 3.14 | 适配 Django 4.2 |
| djangorestframework-simplejwt | 5.3 | JWT 认证 |
| mysqlclient | 2.2 | 连接 MySQL 的驱动 |
| uWSGI | 2.0.23 | 生产部署 |
| Nginx | 1.24 | 反向代理和静态文件服务 |
| MySQL | 8.0 | 数据库 |
这套组合基本无坑,可以放心照着搭。如果你执意用 Python 3.13,可能会遇到 mysqlclient 编译失败的问题,没必要在这个环节浪费一整天。
5.2 本地开发联调的一个小建议
小程序开发者工具默认请求 localhost 会被拦截,需要在「详情」→「本地设置」里勾选“不校验合法域名”。我建议后端开发时启动 Django 的 runserver 0.0.0.0:8000,让手机和电脑连同一个局域网,小程序真机调试模式填 http://局域网IP:8000,调试体验远比开发者工具模拟器流畅,而且能顺手测到手机摄像头和文件上传的真实表现。
5.3 交付时资料要带全
这类项目如果走招投标或毕业设计交付,除了代码,一定要附带以下文档:
- 数据库设计说明书(表结构 + ER 图)
- 接口文档(推荐用 Apifox 或 DRF 的 Schema 自动生成)
- 部署手册(从服务器初始化到 HTTPS 配置的完整步骤)
- 操作手册(针对学工处老师和小程序学生的两种版本)
- 测试报告(功能测试要点 + 并发压测结果)
我见过不少项目死在文档交付这一关,代码写得好但没人看得懂,接手的人全凭猜,最后只能重做。这套文档虽然花时间,但能体现专业度,也能让学校后续维护时真正用得起来。
