第一次看到“nodejs基于django微信小程序的设备报修管理系统”这个课题名时,我的第一反应是:这技术栈写反了吧?Node.js和Django都是后端框架,正常理解里它们是竞品关系,怎么能“基于”到一起?
但等我真的把一个设备报修管理系统从零搭起来之后,发现这个看似拧巴的组合,在真实业务里反而各司其职。Django负责核心业务API,Node.js做实时消息推送和前端工具链支撑,微信小程序负责用户端交互——三个技术角色落在同一个系统里并不冲突。这篇文章就完整记录一下这个系统的设计实现过程,包括架构选型、数据模型、小程序端实现、Node.js辅助服务,以及我在环境配置和联调阶段踩过的坑。如果你正在准备类似的毕业设计,或者想自己搭一套“小程序+后端”的管理系统,这篇可以直接照着落地。
1. 技术栈定位:Node.js和Django在这个项目里不打架
1.1 为什么会出现“Django + Node.js + 小程序”这种组合
先说结论:在中小型管理系统里,Django和Node.js完全可以共存,关键看各自负责什么。
Django的优势在模型层、ORM和Admin后台。设备报修系统的核心是工单数据,需要做权限校验、状态流转、报表统计,Django的ORM和自带Admin能节省大量开发时间。特别是你还要交设计文档的时候,Django的模型定义本身就等于一份数据库设计文档,答辩时给老师讲起来也顺。
Node.js的定位则完全不同。它在这个系统里是“轻量实时通道”——微信小程序端想知道工单状态有没有变化,如果只靠Django,就只能让前端反复轮询接口。轮询在几十个用户的小项目里没毛病,但体验一般、消息不即时。Node.js起一个WebSocket服务,Django收到状态变更后回调Node.js,由它实时推送给对应的小程序端,整个体验从“每隔几秒刷新一下”变成“手机震一下”。
另一个容易被忽略的角色是前端工具链。原生微信小程序开发不强制用Node.js,但如果你像我一样用npm管理小程序依赖、跑辅助脚本,那本机就必须有Node.js环境。很多同学的课题名里写着Node.js,实际上只是因为要跑npm命令。
1.2 系统功能模块和信息流
报修系统有三个角色。报修人——教师、学生、员工,打开小程序报修;维修师傅——接单后上门处理并回填结果;管理员——负责审核派单和统计报表。
核心流程是一条闭环:报修人扫码或选择设备,提交故障描述和照片,Django创建工单,管理员在后台审核并指派维修师傅,Node.js收到通知推送给师傅,师傅接单、标记维修中、填写处理结果,Django更新状态,Node.js再通知报修人,报修人确认并评价,工单结束。
这套流程里所有业务数据都落在Django,Node.js只做状态变化的“传话筒”。模块和接口的对应关系可以用一张表列清楚:
| 端侧 | 模块 | 主要接口 |
|---|---|---|
| 小程序 | 登录 | /api/auth/login |
| 小程序 | 设备列表 | /api/devices/ |
| 小程序 | 提交报修 | /api/repairs/ |
| 小程序 | 我的工单 | /api/repairs/my/ |
| 小程序 | 工单评价 | /api/repairs/{id}/review/ |
| 维修师傅端 | 接单/状态更新 | /api/repairs/{id}/status/ |
| 管理员 | 派单/统计 | /api/admin/... |
| Node.js | WebSocket推送 | ws://host:3001 |
1.3 工程初始化与目录结构
我的工程结构大致是这样:
repair-system/ ├── backend/ # Django 主服务 │ ├── manage.py │ ├── config/ # settings, urls │ └── apps/ │ ├── users/ │ ├── devices/ │ └── repairs/ ├── miniapp/ # 微信小程序原生工程 │ ├── pages/ │ ├── utils/ │ └── app.js └── notifier/ # Node.js WebSocket 辅助服务 ├── index.js └── package.jsonDjango部分用常规命令初始化即可:
python -m venv venv venv\Scripts\activate # Windows # Linux/macOS 用 source venv/bin/activate pip install django djangorestframework django-admin startproject config . python manage.py startapp users python manage.py startapp devices python manage.py startapp repairs这里多说一句:app拆分要按业务边界走,不要学教程里把users、devices、repairs全塞进一个app。后面派单权限、工单状态机、设备分类这些逻辑一多,拆开的优势就体现出来了。
2. 环境配置高频翻车点:npm、Django与开发者工具
2.1 Windows下npm.ps1报错的主要处理
先讲一个几乎每个Windows同学都会碰见的问题:装完Node.js,在PowerShell里执行npm命令,结果提示:
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。这个不是Node.js装坏了,而是PowerShell的执行策略默认是Restricted,不允许运行.ps1脚本。解决办法是在PowerShell里执行:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned然后按提示输入Y确认。这句命令的意思是:当前用户创建的脚本可以运行,从网上下载的脚本需要有可信签名才能运行。改完之后重新打开终端窗口,npm就能正常运行了。
如果不想改执行策略,也可以直接用cmd或Git Bash来跑npm,很多同学在VSCode里用默认PowerShell,切换成cmd同样能跑。这类报错看起来吓人,其实是系统环境策略问题,和项目代码没有关系。
2.2 Django环境准备与创建app
安装Django时建议先建虚拟环境,不然多个项目在同一台机器上互相干扰。Windows下激活虚拟环境用venv\Scripts\activate,Linux/macOS用source venv/bin/activate。依赖我一般这样装:
pip install django djangorestframework django-cors-headersdjango-cors-headers在小程序项目里必须装。虽然小程序真机请求本身不受浏览器CORS策略限制,但你在微信开发者工具里调试网页端管理后台时,跨域问题一定会出现,有了这个中间件就能少折腾。
装完之后创建项目和应用,命令在第1章已经给过了。还有一个新手经常卡住的点:创建完app之后必须去settings.py的INSTALLED_APPS里注册,否则makemigrations识别不到模型:
INSTALLED_APPS = [ # ... 'rest_framework', 'corsheaders', 'users', 'devices', 'repairs', ]2.3 微信开发者工具的初始配置
导入小程序工程到微信开发者工具时,AppID这里有一个分岔:如果只是本地测试,可以直接选择测试号,不注册也能跑;但要真机预览、发布体验版,就必须有一个小程序账号。
这里提醒一下选题的同学:申请小程序时,个人主体和企业主体的权限差别很大。个人主体不能开通微信支付,部分接口(比如手机号快速验证的组件)不可用,会直接影响你设计“获取手机号登录”的功能;企业主体则需要每年交一笔认证费用。学生做毕设,我建议优先用测试号,或者找有企业主体的单位挂靠一下,把精力放在报修业务本身,而不是卡在账号权限上。
还有一个非常影响开发效率的配置:在开发者工具右上角“详情”里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”。否则你的小程序请求一个http://127.0.0.1:8000这种本地地址,马上就会被拦截。等要发布体验版时,再把正式域名配置到微信后台。
关于顶部导航栏,如果要用自定义导航栏,需要在app.json里配置"navigationStyle": "custom",然后在页面里动态计算安全区高度。这部分在第4章展开讲,因为报修工单列表页要配合筛选tab,自定义导航栏几乎必然要用。
3. 设备报修核心数据模型与Django ORM踩坑记录
3.1 四张核心表的设计
设备报修系统的数据模型并不复杂,但字段设计直接影响后面所有接口。我按真实场景来拆。
首先是DeviceCategory,设备分类。很多系统把分类做成两级树,但对报修场景来说一级就够了。常见的分类有:教学一体机、投影仪、打印机、空调、饮水机、网络设备。
然后是Device。设备需要绑定的字段包括:名称、编码、位置、所属分类、当前状态(正常/报修中/停用)。为什么要编码?因为报修人要扫码报修,你把设备编码做成贴纸贴在设备上,小程序里扫一下就能定位设备,比让用户从长列表里手工选择优雅得多。
核心表RepairOrder设计成下面这样:
class RepairOrder(models.Model): STATUS_CHOICES = [ (1, '待派单'), (2, '已派单'), (3, '维修中'), (4, '已完成'), (5, '已评价'), ] device = models.ForeignKey('devices.Device', on_delete=models.PROTECT, verbose_name='设备') reporter = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='reported_orders', verbose_name='报修人') assignee = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name='assigned_orders', verbose_name='维修师傅') description = models.TextField('故障描述') images = models.JSONField('报修图片', default=list) status = models.IntegerField('状态', choices=STATUS_CHOICES, default=1) result = models.TextField('处理结果', blank=True) rating = models.IntegerField('评分', null=True, blank=True) is_deleted = models.BooleanField('逻辑删除', default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True)报修图片用JSONField存URL数组,比另建一张图片表省事。小型系统里图片文件直接放服务器目录,URL存数据库就行,不要一开始就上云存储,那是过度设计。用户角色字段可以在Django自带的User表上扩展,用OneToOne关联一份Profile,加上“报修人/维修师傅/管理员”三种角色即可。
3.2 工单状态流转的设计
状态流转在这类系统里是最容易写乱的地方。我的做法是:不在模型层写死状态机,而是在Service层做状态校验,核心是维护一张允许变更的映射表。
比如:待派单只能变成已派单或取消;已派单只能变成维修中;维修中只能变成已完成;已完成只能变成已评价。凡是非法流转直接返回400错误。这样写比在模型里堆一堆逻辑更直观,排查问题时也容易定位。
另一个关键点是身份校验:维修师傅端更新状态时,接口必须先确认当前登录用户的角色是维修师傅,并且工单的assignee就是这个人,不然任何人都能篡改工单状态。我第一版顺手写了个“测试专用”的全员可改接口,结果内测时被同事把所有工单状态改成了已完成,教训深刻。
3.3 Django ORM查询与删除对象的坑
先讲查询优化。报修工单列表如果直接RepairOrder.objects.all(),序列化时每行都要查一次device、reporter、assignee,数据一多就是典型的N+1查询问题。用select_related把外键一次查出来:
RepairOrder.objects.filter(is_deleted=False).select_related('device', 'reporter', 'assignee')这也是你在搜索“django执行查询”相关问题时最常遇到的关键字。查询性能问题80%都出在N+1上,先学会这个,比学一堆查询技巧都实在。
再讲删除。Django的删除操作有三个容易忽略的点。
第一,QuerySet批量删除不会调用模型类里重写的delete()方法,只有单个对象删除才会调用。所以如果你重写了delete()做文件清理之类的事,记得用for obj in queryset逐个删,或者借助信号机制。
第二,on_delete=models.CASCADE的级联删除很危险。我在维修记录相关的表上刻意用了PROTECT和SET_NULL,就是防止误删。报修人注销账号,报修工单不能被级联删掉,这样整条工单追溯链就断了。
第三,报修工单这种业务数据千万别物理删除。管理员误操作删单,数据就真的没了。我的做法是加is_deleted字段,配合一个默认管理器自动过滤:
class RepairOrderManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(is_deleted=False) class RepairOrder(models.Model): # ... objects = RepairOrderManager()这样代码里写RepairOrder.objects.all()时,逻辑删除的数据就被自动屏蔽了。只有在极少数审计场景下才用all_objects去查原始数据。
4. 小程序端报修链路:登录、表单、图片上传与分页加载
4.1 登录与会话保持
微信小程序里没有传统账号密码。它的登录链路是:wx.login获取临时code,把code发给后端,后端调用微信的code2Session接口,拿到openid和session_key。openid是用户在小程序生态里的唯一身份。
我直接把openid作为Django侧User表的唯一标识,首次登录自动创建用户。接口设计成这样:
# 伪代码 def login(request): code = request.data.get('code') # 用 requests 调微信 code2Session data = wx_api.code2session(code) user, _ = User.objects.get_or_create(openid=data['openid']) token = generate_token(user) return JsonResponse({'token': token, 'role': user.role})前端拿到token之后,后续所有请求在header里带上Authorization: Token xxx,Django端用一个简单的token中间件解析出当前用户即可。
关于手机号,很多同学都在搜“微信小程序登录获取手机号”。我提一个现状:现在获取微信手机号需要通过按钮组件触发getPhoneNumber事件,而且个人主体小程序用不了这个能力。所以我在报修系统里做了妥协方案——登录仍然用openid,每个用户在“个人中心”可以手动填写手机号,存到User表。这样做既绕开了能力限制,又满足了维修师傅联系报修人的实际需求。
4.2 报修表单与图片上传
报修页面是整个项目里最核心的页面。设备选择用picker组件,数据来自设备列表接口。这里有一个小技巧:在设备列表里直接过滤掉状态为“报修中”的设备,避免用户对同一台设备反复提交报修。
图片上传是小程序端最容易出问题的环节,标准流程是这样:
wx.chooseMedia({ count: 3, mediaType: ['image'], success: async (res) => { for (const file of res.tempFiles) { wx.uploadFile({ url: BASE_URL + '/api/upload/', filePath: file.tempFilePath, name: 'file', success: (res) => { const data = JSON.parse(res.data); imageUrls.push(data.url); } }); } } });后端Django接收file字段并保存:
def upload(request): f = request.FILES['file'] path = os.path.join('uploads', f.name) with open(path, 'wb') as dest: for chunk in f.chunks(): dest.write(chunk) return JsonResponse({'url': '/media/' + f.name})提交报修时,把图片URL数组和表单内容一起POST,后端直接存JSONField。这里有一个高频坑:wx.uploadFile里name字段的值必须和后端request.FILES的键一致,否则上传接口会一直报错“找不到文件字段”。不少人在这个对上半天,以为是自己的上传逻辑写错了。
4.3 报修单列表与加载更多
“我的工单”列表需要分页,对应的就是“页面列表加载更多”这个常见需求。小程序端的实现套路很固定:
Page({ data: { page: 1, list: [], hasMore: true, loading: false }, onReachBottom() { if (this.data.loading || !this.data.hasMore) return; this.loadMore(); }, loadMore() { this.setData({ loading: true }); wx.request({ url: BASE_URL + '/api/repairs/my/', data: { page: this.data.page, page_size: 10 }, success: (res) => { const { results, has_more } = res.data; this.setData({ list: this.data.list.concat(results), hasMore: has_more, page: this.data.page + 1, loading: false }); } }); } });关键点有两个:第一,onReachBottom触发时要加loading锁,不然滚到底部会连续触发多页请求,把列表搞乱;第二,后端接口要返回has_more,不要只靠res.data.length === page_size判断,遇到最后一页刚好10条时会多发一次无效请求。
4.4 顶部导航栏高度与页面适配
“微信小程序顶部导航栏高度”这个搜索词热度很高,搜它的人多半在做自定义导航栏,被iPhone和安卓的胶囊按钮位置差异折磨。
正确的做法是用wx.getWindowInfo()获取窗口信息,再结合wx.getMenuButtonBoundingClientRect()获取胶囊按钮位置,计算出导航栏高度:
function getNavBarHeight() { const win = wx.getWindowInfo(); const menu = wx.getMenuButtonBoundingClientRect(); const navBarHeight = (menu.top - win.statusBarHeight) * 2 + menu.height; return { statusBarHeight: win.statusBarHeight, navBarHeight: navBarHeight }; }这个高度在页面onLoad时计算一次,然后撑开自定义导航栏的占位视图。设备报修系统的工单列表页我加了一个筛选tab(全部/待派单/维修中/已完成),自定义导航栏配合这个tab,比原生导航灵活很多,但也正是这个自定义导航栏,让我在适配各种机型上多花了两天时间。
5. Node.js辅助服务实战:维修进度实时通知怎么落地
5.1 为什么通知不能只靠轮询
如果小程序端每隔几秒请求一次“我的工单”接口,会有几个问题:用户量稍微上来,Django接口压力变大;手机端耗电变快;用户看到的状态更新还是延迟的。报修场景里,用户提交工单后最关心的就是“师傅有没有接单”“处理到哪一步了”。我的优化思路就是用Node.js做一个WebSocket推送服务,把状态变更从“用户主动查”变成“服务端主动推”。
这个服务很小,职责很单一:维护所有在线用户的连接,接收Django发来的变更事件,找到对应用户的连接并推送消息。
5.2 一个极简的WebSocket推送服务
我用ws库实现,Node.js代码量在100行以内:
const WebSocket = require('ws'); const http = require('http'); const wss = new WebSocket.Server({ port: 3001 }); const userMap = new Map(); // userId -> Set<ws> wss.on('connection', (ws, req) => { const userId = parseUserIdFromUrl(req.url); if (userId) { if (!userMap.has(userId)) userMap.set(userId, new Set()); userMap.get(userId).add(ws); } ws.on('close', () => { userMap.get(userId)?.delete(ws); }); }); http.createServer((req, res) => { if (req.url === '/notify' && req.method === 'POST') { let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { const { userId, message } = JSON.parse(body); const conns = userMap.get(String(userId)); if (conns) { conns.forEach(conn => { if (conn.readyState === WebSocket.OPEN) { conn.send(JSON.stringify(message)); } }); } res.end('ok'); }); } }).listen(3000, () => console.log('notifier running'));小程序端连接:
wx.connectSocket({ url: 'ws://192.168.x.x:3001/?userId=' + userId });开发阶段用局域网IP直连很方便,真机调试时手机和电脑连同一个Wi-Fi即可。但生产环境必须把ws换成wss,域名要做备案,并且在微信后台配置socket合法域名。
5.3 Django侧如何对接Node.js
Django工单状态更新后,往Node.js发一个HTTP POST通知:
import requests def notify_user(user_id, message): try: requests.post( 'http://127.0.0.1:3000/notify', json={'userId': str(user_id), 'message': message}, timeout=2 ) except Exception: # 通知失败不能影响主流程 logger.error('notify failed', exc_info=True)有一个细节很重要:Django这边要开线程或者用celery异步调用通知,不要在主进程里同步等待Node.js响应。我第一版直接在视图里同步调requests.post,结果Node.js服务一旦没启动,整个报修提交接口就卡住报错。改成异步之后,就算notifier挂了,工单照常创建,用户只是暂时收不到实时推送。
还有一点和“nodejs怎样隐藏post和端口号”这类搜索相关:Node.js的3000端口不能直接暴露公网。我的做法是生产环境用Nginx把所有请求统一走80/443,/notify路径内部反代到Node.js。这样前端永远只看到一个域名,Node.js服务的端口和内部路径都不对公网可见。
最后分享一个本地调试的小技巧:在桌面建一个start.bat,把Django服务、Node.js服务、微信开发者工具的启动命令放在一起,双击一次全部启动。不然每天开机手动敲三条命令,很容易漏掉其中一个,然后排查半天“为什么推送不生效”。
6. 联调、抓包与体验版分发:让系统真实跑起来
6.1 用Charles抓包排查小程序请求
小程序端报错经常是很笼统的,比如“请求失败”“返回undefined”。光看console有时定位不到问题。我习惯先用微信开发者工具的Network面板看请求状态码,如果后端返回500,再去后端日志查异常。
真机上抓包就要用到Charles了。使用套路是:手机和电脑连同一个Wi-Fi,手机代理指向电脑IP和Charles端口(默认8888),然后在Charles里开启SSL Proxying,把目标域名加进去。这样手机上小程序的每一个HTTPS请求都能看到明文内容,包括请求参数和后端返回。
我在联调阶段遇到过一个问题:真机上提交报修,到图片上传那一步总是失败,开发者工具里却完全正常。用Charles一看,原来是后端配置的媒体域名是http://127.0.0.1:8000,真机根本访问不了这个地址。把图片URL改成电脑的局域网IP之后,问题立刻解决。这种场景没有Charles的话,可能要猜很久。
6.2 体验版分发与试用反馈收集
开发完成后,需要让维修师傅和同事试用。操作步骤不复杂:微信开发者工具点右上角“上传”,填版本号和备注,然后去微信公众平台后台的“版本管理”里把刚上传的版本设为体验版,生成体验版二维码。把二维码发给体验成员,他们扫码就能在微信里打开。
这里有三个容易踩的坑。
第一,不校验合法域名只在开发者工具里有效,真机打开体验版时,所有请求域名必须已经在微信后台配置为request合法域名。我第一版忘了配,真机一打开,接口全被拦截。
第二,上传之前要清掉本地缓存,否则用户拿到体验版,打开的可能是之前缓存的旧页面。
第三,体验成员需要在后台添加。你拿一个普通微信号扫体验版二维码,会提示“无权限”,必须先在“成员管理”里把对方加为体验成员。认证费用那个事在这里也有影响:企业主体认证后,体验版和正式版的功能才完整。
试用反馈收集,我推荐在工单列表页放一个简单的悬浮按钮“问题反馈”,直接提交到后台独立接口。不要依赖微信聊天记录,不然一堆反馈散落在各个对话框,收集和分析都麻烦。
6.3 上线部署前的事项清单
报修系统要真正投入使用,下面这些事越早准备越好:
- HTTPS证书。微信小程序正式版环境强制HTTPS,开发阶段用
http://没事,上线必须换成https://。 - 域名备案。小程序后台配置合法域名时,服务器域名通常要求完成备案,这一步要预留两周以上时间。
- 数据库迁移。Django默认的SQLite做学习项目没问题,正式使用建议切到MySQL或PostgreSQL,并配置定时备份。
- 媒体文件存储路径。报修图片不能随便存在Django根目录下,建议单独挂载数据盘或接入云存储,否则备份时图片容易丢。
这份清单看着琐碎,但每一条都有人在生产环境栽过跟头。
最后说说我做完整个项目的体会。这个系统最大的价值不在于用了多少种技术,而在于它把“报修”这件小事,从口头喊、电话催,变成了一条有记录、可追踪、能评分的完整链路。我在设计时坚持了一个原则:核心业务数据必须永远可追溯,所以工单一律软删除;状态变更必须立刻被感知,所以才会引入Node.js做实时推送。如果要在现有基础上扩展,可以考虑的方向有备件库存管理、维修师傅绩效统计、设备保养计划提醒。先把报修主流程跑通,再逐步加功能,这个顺序永远不会错。