ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django与Node.js联手:微信小程序设备报修管理系统实战解析

Django与Node.js联手:微信小程序设备报修管理系统实战解析 第一次看到“nodejs基于django微信小程序的设备报修管理系统”这个课题名时我的第一反应是这技术栈写反了吧Node.js和Django都是后端框架正常理解里它们是竞品关系怎么能“基于”到一起但等我真的把一个设备报修管理系统从零搭起来之后发现这个看似拧巴的组合在真实业务里反而各司其职。Django负责核心业务APINode.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再通知报修人报修人确认并评价工单结束。这套流程里所有业务数据都落在DjangoNode.js只做状态变化的“传话筒”。模块和接口的对应关系可以用一张表列清楚端侧模块主要接口小程序登录/api/auth/login小程序设备列表/api/devices/小程序提交报修/api/repairs/小程序我的工单/api/repairs/my/小程序工单评价/api/repairs/{id}/review/维修师傅端接单/状态更新/api/repairs/{id}/status/管理员派单/统计/api/admin/...Node.jsWebSocket推送ws://host:30011.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\activateLinux/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_deletemodels.PROTECT, verbose_name设备) reporter models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.PROTECT, related_namereported_orders, verbose_name报修人) assignee models.ForeignKey(settings.AUTH_USER_MODEL, nullTrue, blankTrue, on_deletemodels.SET_NULL, related_nameassigned_orders, verbose_name维修师傅) description models.TextField(故障描述) images models.JSONField(报修图片, defaultlist) status models.IntegerField(状态, choicesSTATUS_CHOICES, default1) result models.TextField(处理结果, blankTrue) rating models.IntegerField(评分, nullTrue, blankTrue) is_deleted models.BooleanField(逻辑删除, defaultFalse) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue)报修图片用JSONField存URL数组比另建一张图片表省事。小型系统里图片文件直接放服务器目录URL存数据库就行不要一开始就上云存储那是过度设计。用户角色字段可以在Django自带的User表上扩展用OneToOne关联一份Profile加上“报修人/维修师傅/管理员”三种角色即可。3.2 工单状态流转的设计状态流转在这类系统里是最容易写乱的地方。我的做法是不在模型层写死状态机而是在Service层做状态校验核心是维护一张允许变更的映射表。比如待派单只能变成已派单或取消已派单只能变成维修中维修中只能变成已完成已完成只能变成已评价。凡是非法流转直接返回400错误。这样写比在模型里堆一堆逻辑更直观排查问题时也容易定位。另一个关键点是身份校验维修师傅端更新状态时接口必须先确认当前登录用户的角色是维修师傅并且工单的assignee就是这个人不然任何人都能篡改工单状态。我第一版顺手写了个“测试专用”的全员可改接口结果内测时被同事把所有工单状态改成了已完成教训深刻。3.3 Django ORM查询与删除对象的坑先讲查询优化。报修工单列表如果直接RepairOrder.objects.all()序列化时每行都要查一次device、reporter、assignee数据一多就是典型的N1查询问题。用select_related把外键一次查出来RepairOrder.objects.filter(is_deletedFalse).select_related(device, reporter, assignee)这也是你在搜索“django执行查询”相关问题时最常遇到的关键字。查询性能问题80%都出在N1上先学会这个比学一堆查询技巧都实在。再讲删除。Django的删除操作有三个容易忽略的点。第一QuerySet批量删除不会调用模型类里重写的delete()方法只有单个对象删除才会调用。所以如果你重写了delete()做文件清理之类的事记得用for obj in queryset逐个删或者借助信号机制。第二on_deletemodels.CASCADE的级联删除很危险。我在维修记录相关的表上刻意用了PROTECT和SET_NULL就是防止误删。报修人注销账号报修工单不能被级联删掉这样整条工单追溯链就断了。第三报修工单这种业务数据千万别物理删除。管理员误操作删单数据就真的没了。我的做法是加is_deleted字段配合一个默认管理器自动过滤class RepairOrderManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(is_deletedFalse) 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(openiddata[openid]) token generate_token(user) return JsonResponse({token: token, role: user.role})前端拿到token之后后续所有请求在header里带上Authorization: Token xxxDjango端用一个简单的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 - Setws 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.jsDjango工单状态更新后往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}, timeout2 ) except Exception: # 通知失败不能影响主流程 logger.error(notify failed, exc_infoTrue)有一个细节很重要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做实时推送。如果要在现有基础上扩展可以考虑的方向有备件库存管理、维修师傅绩效统计、设备保养计划提醒。先把报修主流程跑通再逐步加功能这个顺序永远不会错。
返回列表