1. 项目整体设计思路1.1 核心需求拆解与功能边界设备报修这事但凡在稍微正规一点的公司、学校、厂区里待过就知道传统报修方式有多折腾。打电话报修说不清位置微信群里报修消息被刷掉报修完不知道进度维修人员跑错地方。这套系统要解决的就是这些真实痛点核心需求拆开看就是四个闭环报修发起端员工或用户通过微信小程序扫码或手动选择设备上传故障描述、图片提交报修单实时看到处理进度。处理端管理员或维修人员在后台PC端Vue页面看到工单流进行接单、派单、处理、完结操作。通知端工单状态变化时通过微信订阅消息或WebSocket实时推送给相关人不用反复刷新。统计端按设备类型、故障类型、处理时长等维度做基础统计给管理者决策参考。这四个闭环彼此独立又相互依赖。报修发起端解决怎么报修的问题处理端解决谁来修、修没修的问题通知端解决信息同步的问题统计端解决提升效率的问题。项目名里列出了PHP和Node.js两套后端技术实际就是这个系统里不同环节的职责分配后面细说。1.2 为什么选这套技术栈首先说微信小程序这是报修系统的天然入口不需要用户装App微信里扫码就能打开权限体系和登录体系都是现成的。小程序端我用Uniapp而不是原生小程序核心原因是一套代码可以同时编译到微信小程序、H5、App。说实话很多报修后台的管理人员偶尔需要在手机上处理工单用H5版就能解决没必要再维护一套原生代码。后端我选了PHP负责主要业务接口Node.js做辅助服务。很多人问为什么不干脆只用一种我的理解是PHP处理常规的增删改查、文件上传、权限校验非常成熟生态里现成的东西多部署也简单不管是宝塔还是Docker跑起来都不费劲。而Node.js在实时通信方面有天然优势WebSocket推送、定时任务扫描、消息队列处理这些用Node.js写起来比PHP顺手得多。两个服务之间通过HTTP接口内部调用各干各擅长的活这是这套架构的核心逻辑。管理后台用Vue配合Element UI或类似组件库开发效率非常高。Vue的双向绑定和组件化机制特别适合工单列表、状态流转这类交互密集的后台页面。整个技术栈选型的核心原则就一句话用最顺手的技术处理最擅长的环节而不是追求全栈统一使用同一种语言。2. 前后端架构与关键模块设计2.1 数据库表结构设计报修系统的核心数据表我拆成了六张不多不少业务恰恰够用。闯过坑之后发现表结构设计千万别一开始就整一堆冗余字段后面改动成本太高。用户表user存储微信用户的openid、unionid、昵称、头像、手机号、角色普通用户/维修工/管理员。openid是微信生态的唯一标识登录环节的核心凭证。注意手机号字段建议独立存储因为微信手机号获取是一次性加密数据需要后端配合解密。设备表device设备编号、名称、型号、位置、所属区域、状态正常/报修中/停用、二维码标识。这里有个细节位置字段要存两级以上比如3号楼-2层-会议室因为报修时要按位置快速定位和筛选。报修工单表repair_order工单号、报修人ID、设备ID、故障类型、故障描述、图片URL、状态待接单/处理中/已完成/已取消、紧急程度、报修时间、完成时间。这张表是整个系统的核心状态流转逻辑都在这一张表上。工单日志表order_log工单ID、操作人ID、操作类型、操作内容、操作时间。每次状态变更都记录一条日志方便后续排查问题和管理审计。通知记录表notification接收人ID、通知类型、内容、是否已读、创建时间。WebSocket推送的消息和微信订阅消息统一在这张表登记。评价表evaluation工单ID、报修人ID、评分、评价内容、评价时间。工单完结后报修人可以评价反向督促维修质量。这套表结构看着简单但每张表都有存在的必要。核心原则是宁可多一张日志表也不要把状态变化埋在代码里。没有日志表时曾经出过一次问题一个工单从处理中直接变成已完成用户投诉维修工没来我们查了半天没有记录可查。加了日志表之后每次状态变更都有迹可循类似纠纷再没发生过。2.2 小程序端模块拆解Uniapp开发微信小程序我按页面和功能模块拆成这几块登录模块微信小程序最关键的环节。流程是wx.login获取code传给后端调用微信的code2Session接口换openid和session_key。注意手机号获取方式在微信新版API里改了现在是用户点击button触发getPhoneNumber后端拿code解密获取手机号不能直接用旧接口了。这个坑后面详细说。首页与设备选择首页展示报修入口扫码识别设备、手动搜索设备、常用设备列表三种方式。扫码功能用uni.scanCode接口扫到的二维码内容就是设备编号。手动搜索设备支持模糊匹配设备名称和设备编号。报修工单填写页核心交互页面。选择故障类型下拉选择、填写故障描述textarea、上传故障照片uni.chooseImage配合uni.uploadFile。这里有一个交互细节故障描述默认给几个常用语模板让用户快速选择比如设备无法开机、设备运行时异响、设备屏幕显示异常有效减少用户打字负担。工单列表与详情页我的报修列表页分进行中和已完成两个Tab。需要做下拉刷新和上拉加载用Uniapp的onPullDownRefresh和onReachBottom。详情页展示工单状态流转时间线状态变化有记录。实时状态更新通过WebSocket推送后前端刷新如果没有WebSocket连接就做一个手动下拉刷新兜底。个人中心页展示用户信息、我的设备收藏的常用设备、我的评价、联系客服入口。小程序端的核心逻辑其实不复杂但处理好在弱网环境下的体验不容易。比如上传图片失败要能续传工单提交失败要在本地草稿箱保存重新联网后提示用户重试。这些措施能显著降低用户流失率。2.3 PHP与Node.js的职责划分这两个后端服务的分工我画一个简单的请求路径图来理解微信小程序Uniapp → PHP API业务逻辑 → MySQL ↓ 内部调用 Node.jsWebSocket推送 / 定时任务 / 导出PHP负责的模块用户认证接口登录、手机号绑定、角色鉴权。工单管理接口创建工单、查询工单、更新工单状态、取消工单。设备管理接口设备列表、设备详情、设备绑定、二维码生成。评价接口提交评价、查看评价。管理后台接口用户管理、设备管理、工单分配、统计报表。PHP的技术选型我建议用ThinkPHP或Laravel。ThinkPHP上手快中文文档友好中小型项目够用Laravel生态更完整但学习曲线稍陡适合对代码规范和架构要求高的团队。我这个项目用的ThinkPHP6理由很简单业务复杂度不需要Laravel的重型功能ThinkPHP6的部署和维护成本更低路由、ORM、中间件都有够了。Node.js负责的模块WebSocket推送服务连接管理、事件广播、断线重连。定时任务超时工单检测比如超过2小时未接单自动提醒、每日数据汇总。文件导出服务工单Excel导出、月度统计报表导出。Node.js我用的Express框架代码量不大核心就是维护一个WebSocket连接池。PHP处理完业务后如果需要通知用户就请求Node.js的推送接口Node.js把消息转发给对应的小程序WebSocket连接。实测下来这套流程很稳定延迟在毫秒级。为什么不让PHP直接做推送PHP实现WebSocket不是不行但常驻内存的进程管理和长连接维护不如Node.js顺手。PHP更适合请求-响应模式Node.js更适合长连接-事件模式。让两种技术做各自擅长的事这是架构设计的核心思想。当然也有一个现实原因小程序端和Web端需要实时收到工单状态变化用轮询会大量浪费服务器资源和用户流量WebSocket是一次连接、随时推送两者体验差得很远。3. 核心功能实操实现3.1 微信登录与手机号授权实操这一步是新手上路第一个大坑。微信小程序登录流程拆开看只有三步但每一步的细节都容易踩坑。第一步前端获取codeuni.login({ provider: weixin, success: (loginRes) { // 获取到临时code const code loginRes.code; // 把这个code发给后端 this.$http.post(/api/auth/login, { code: code }); } });第二步后端换openidPHP后端拿code请求微信接口$url https://api.weixin.qq.com/sns/jscode2session?appid{$appid}secret{$secret}js_code{$code}grant_typeauthorization_code; $result file_get_contents($url); $data json_decode($result, true); // $data[openid] 用户唯一标识 // $data[session_key] 会话密钥用于解密手机号注意appid和secret放在服务器端配置文件中绝对不能写在小程序前端代码里。之前见过有人把secret写死在js里结果被安全扫描工具直接扫出来整个接口裸奔这是最严重的低级错误。第三步获取手机号新版微信手机号是加密的流程是前端点击button触发用户授权后拿到code把code传给后端后端调接口解密button open-typegetPhoneNumber clickgetPhoneNum授权手机号/button // 前端逻辑 getPhoneNum(e) { if (e.detail.code) { this.$http.post(/api/auth/phone, { code: e.detail.code }); } }// 后端解密手机号 $url https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token{$access_token}; $data json_encode([code $code]); // 返回数据包含 phone_info.phoneNumber 和 purePhoneNumber这里有个容易忽略的点获取手机号需要access_token而access_token需要通过appid和secret换取并且有效期为7200秒。所以封装一个token管理方法用缓存存token过期自动刷新不然每次请求都去换取会有限流风险。实操心得先做静默登录code2Session再按需授权手机号不要让用户一打开小程序就被迫授权手机号授权通过率会大幅下降。3.2 报修工单状态机与创建流程工单状态是整个系统的核心逻辑。我设计了五个状态流转关系必须清晰待接单(0) - 处理中(1) - 已完成(2) | | v v 已取消(3) 已取消(3)状态机说明待接单用户提交报修单后进入此状态维修工或管理员看到待接单列表。处理中维修工接单标记开始处理。此时报修人能看到是谁在处理。已完成维修工提交完成填写处理结果报修人可评价。已取消用户主动取消或超时未处理系统自动取消。状态变更不可逆跳比如待接单不能直接变成已完成必须经过处理中否则维修记录就失真了。创建工单的后端代码PHPpublic function createOrder($userId, $deviceId, $faultType, $description, $images) { // 事务控制保证数据一致性 Db::startTrans(); try { // 生成唯一工单号 $orderNo WO . date(YmdHis) . rand(1000, 9999); // 插入工单主表 $orderId Db::name(repair_order)-insertGetId([ order_no $orderNo, user_id $userId, device_id $deviceId, fault_type $faultType, description $description, images json_encode($images), status 0, create_time time() ]); // 写入操作日志 Db::name(order_log)-insert([ order_id $orderId, operator_id $userId, action create_order, content 用户提交报修工单, create_time time() ]); // 通知管理员有新工单 $this-notifyAdmins($orderId); Db::commit(); return $orderId; } catch (\Exception $e) { Db::rollback(); throw $e; } }事务必须用上。因为工单创建涉及主表插入、日志表插入、通知记录表插入三步操作中间任何一步失败都会导致数据不一致。用事务包起来要么全部成功要么全部回滚。创建工单时还需要同时触发Node.js推送private function notifyAdmins($orderId) { // 调用Node.js推送服务 $nodeUrl http://127.0.0.1:3000/api/push/order; $postData json_encode([ type new_order, order_id $orderId ]); $ch curl_init($nodeUrl); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); curl_setopt($ch, CURLOPT_HTTPHEADER, [Content-Type: application/json]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_exec($ch); curl_close($ch); }这里有个性能隐患每次创建工单都同步调用Node.js推送接口如果Node.js服务挂了PHP端会一直等待。解决方法是设置curl超时时间为2秒并且推送失败不影响主流程写在日志里即可。更稳妥的方案是引入消息队列但项目初期没必要2秒超时足够兜底。3.3 Vue管理后台实操管理后台是给管理员和维修工用的核心页面四个工作台数据总览、工单管理、设备管理、用户管理。工作台放几个统计卡片今日新报修数、待处理工单数、处理中工单数、本月完成率。用一个折线图展示近7天的报修趋势。图表库用EChartsVue2配合vue-echartsVue3可以用echarts官方的新写法。工单管理是核心页面。列表筛选条件包括状态、紧急程度、故障类型、时间段。列表操作按钮按状态显示待接单的显示接单处理中的显示完成已完成的不显示操作按钮。Vue的组件化在这里特别舒服工单卡片可以复用于列表和详情。接单操作的核心逻辑async handleAccept(orderId) { const res await api.updateOrderStatus({ order_id: orderId, status: 1, operator_id: this.userInfo.id }); if (res.code 0) { this.$message.success(接单成功); this.loadOrderList(); // 刷新列表 } }Vue后台这里有几个细节值得注意状态筛选用Tabs而不是下拉框工单管理页顶部用Tab切换全部/待接单/处理中/已完成/已取消比下拉框直观操作路径短。列表必须有分页工单量一大一次性加载几百条会卡顿。统一用分页组件每页20条配合后端LIMIT分页。权限控制普通维修工只能看到分配给自己的工单管理员能看到全部工单。后端接口返回数据时就要过滤不能只靠前端隐藏按钮前端永远不可信。3.4 环境搭建与部署实操整套系统的部署环境我用的是Linux服务器 Nginx PHP 7.4 MySQL 5.7 Node.js 14。PHP环境配置PHP安装这里踩过不少坑。最典型的就是用源码编译安装时提示no package libzip found这是因为编译安装PHP 7.4以上版本需要libzip库而系统自带的版本太老。解决办法是先把libzip编译安装好# 下载编译libzip wget https://libzip.org/download/libzip-1.7.3.tar.gz tar -zxvf libzip-1.7.3.tar.gz cd libzip-1.7.3 mkdir build cd build cmake .. make make install装好之后再编译PHP--with-zip参数就能正常识别了。如果是在CentOS上用宝塔面板的面板自带PHP不用这么折腾直接可视化安装。Node.js环境配置Node.js的坑主要在Windows开发机上。很多人第一次用npm就报这个错npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本这个错误是PowerShell的执行策略禁止运行脚本导致的。解决办法有两种# 方法一:以管理员身份运行PowerShell修改执行策略 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser # 方法二:使用CMD而不是PowerShell npm -v方法一更彻底改完之后PowerShell和VS Code的终端都能正常用npm命令。Nginx配置PHP项目配置一个server块Node.js项目配置一个server块做反向代理。关键是跨域问题管理后台Vue运行在http://admin.example.comPHP接口运行在http://api.example.com必然产生跨域请求。PHP端加响应头解决header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization);如果携带了自定义header比如token必须写Access-Control-Allow-Headers否则前端请求被浏览器拦截这个问题排查起来特别隐蔽。4. 常见问题与排查方法4.1 微信小程序端问题实录问题一导航栏高度在不同机型上错位。微信小程序的顶部导航栏在不同机型上高度不一样尤其是刘海屏和普通屏差距明显。Uniapp里可以用uni.getSystemInfoSync()获取状态栏高度然后动态设置自定义导航栏高度const systemInfo uni.getSystemInfoSync(); this.statusBarHeight systemInfo.statusBarHeight; this.navBarHeight systemInfo.statusBarHeight 44; // 44是导航栏标准高度注意statusBarHeight在Android和iOS上返回的值单位不同建议都转成px处理用uni.upx2px进行单位换算。问题二小程序开发工具真机预览二维码扫了打不开。多半原因是开发版小程序的域名白名单没配好或没开启调试模式。真机预览时要在小程序开发者工具的详情-本地设置里勾选不校验合法域名否则所有接口请求都会被拦截。注意这只是开发阶段的手段上线前必须配置合法域名并把微信服务器域名白名单配好。问题三uniapp打包微信小程序后样式错乱。原因是Uniapp编译后的rpx转换和组件的样式隔离。排查方法开发者工具里打开样式隔离选项或者检查是不是用了非标准的CSS属性。rpx的动态计算在不同机型上最容易出问题建议固定宽度尽量用百分比高度用rpx。4.2 PHP端问题实录问题一跨域请求前端能请求通但带不了cookie/header。前面说了要在PHP端加响应头。但还有个容易忽略的点如果前端需要携带自定义header比如Authorization浏览器会先发一个OPTIONS预检请求。PHP接口必须对OPTIONS请求直接返回200否则真正的GET/POST请求会被拦截。ThinkPHP中可以这样处理if ($_SERVER[REQUEST_METHOD] OPTIONS) { header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With); exit; }问题二PHP 8环境下老项目报错vcruntime140.dll不兼容。这是Windows下的历史遗留问题。调试时建议用php -v检查当前PHP版本如果项目代码用的是PHP 7语法直接切换到PHP 7.4版本最省事。配置PHPStorm时要在Settings-PHP里把Interpreters指向正确的PHP可执行文件否则IDE的语法检查和实际运行环境不一致会误判很多语法错误。问题三文件上传报错upload_tmp_dir不可写。上传故障图片时遇到File upload error - unable to create a temporary directory排查思路确认php.ini里的upload_tmp_dir目录存在且有写权限upload_max_filesize和post_max_size也要调整建议都设为20M以上。部署在Nginx上时还要同步检查Nginx的client_max_body_size默认是1M不改的话大图直接传不上去。4.3 Node.js端问题实录问题一npm安装依赖慢或安装失败。国内直连npm官方源经常超时配置淘宝镜像npm config set registry https://registry.npmmirror.com问题二Node.js进程挂了WebSocket断开。这是生产环境最严重的问题。比如服务器内存不足时Node.js进程被系统杀掉所有小程序的WebSocket连接全部断开。解决方案有两个层面代码层面前端检测到WebSocket断开后自动重连设置重连间隔指数退避。运维层面用PM2管理Node.js进程pm2 start app.js --name repair-push pm2 save pm2 startupPM2的daemon模式可以让Node.js应用开机自启、崩溃自动重启这是必须的基础配置不能省。问题三Node.js监听端口被封或冲突。线上部署时Node.js监听3000端口但很多服务器只放行80和443端口。解决方案是Nginx反向代理外部请求wss://example.com/ws转发到http://127.0.0.1:3000外部直接访问不到3000端口安全性也好一些。location /ws { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; }这一层反向代理配置很重要。WebSocket需要Upgrade和Connection头部Nginx默认不会转发这两个头不配置的话WebSocket握手必然失败。4.4 联调问题实录问题一小程序无法请求本机开发的API。Uniapp开发阶段在手机预览时手机和小程序开发者工具连接的是同一个局域网请求地址要写电脑的局域网IP不能写localhost。比如电脑IP是192.168.1.100接口地址就是http://192.168.1.100:8080/api。另外HTTP明文请求在iOS的App环境下默认会被拦截微信小程序也有体验版的域名校验开发阶段使用不校验合法域名选项解决。问题二接口调试时请求参数被URL编码后端取不到值。排查方法先在后端打印收到的原始数据file_get_contents(php://input)看看实际传过来的是什么格式。如果是JSON格式用json_decode解析不要用$_POST去取。问题三联调过程中工单状态不同步。典型案例后台改了工单状态小程序端的列表数据还是旧的。原因有两个可能一是小程序列表页面做了缓存二是WebSocket消息没到达或前端没处理。排查思路先在浏览器手动请求接口确认数据源正确再看WebSocket连接状态最后看小程序的onShow有没有做下拉刷新。我给小程序详情页加了两个机制页面onShow时调用一次uni.request刷新详情同时通过WebSocket收到状态变更事件时主动更新页面数据。双保险效果很好基本不会出现数据滞后超过5秒的情况。5. 实操心得与改进方向整套系统从立项到上线前后用了不到三周。给我的感受是技术选型不是越新越好而是越顺手越好。微信小程序解决了入口和用户身份问题Uniapp解决多端复用问题PHP解决了业务快速交付问题Node.js解决了实时性要求Vue解决了后台开发效率问题。每一层都是够用就行、各司其职。我实际操作下来有几个体会想分享第一个体会是状态机设计一定要先想清楚再写代码。工单状态流转是整个系统的灵魂如果状态设计不清晰后面的接单、派单、统计、通知全都会跟着乱。建议在白板上先把状态图画出来所有可能的状态转换都列出来再开始编码。第二个体会是日志记录比想象中重要得多。不只是工单日志PHP和Node.js的运行日志、接口调用日志都要有。生产环境出了问题日志是唯一的线索。我遇到过Node.js推送偶发失败的问题就是通过日志发现是服务器内存不足导致进程被系统杀掉定时任务连不上。没有日志排查这个问题可能要浪费一下午。第三个体会是前端要时刻考虑弱网和异常场景。小程序端在食堂、地下车库、电梯里使用频率很高信号不佳是常态。所以上传图片要有失败重试提交工单要有草稿保存列表加载要有加载中状态和加载失败重试按钮。这些细节决定了用户对系统的整体评价。这套系统后续还可以扩展的方向不少。比如引入故障自动分类根据报修描述自动打标签或者给维修工加一个类似抢单的模式待接单工单推送出来后先到先得再或者把评价体系和维修工的绩效考核挂钩形成正向激励循环。整体架构不变在这些模块上叠加即可。最后再说一个小技巧报修系统这种业务二维码是入口的关键。给每台设备生成专属二维码打印出来贴在设备上用户扫一下就能进入报修页面设备信息自动带出这一步体验做好整个报修流程就顺畅了一大半。二维码生成用PHP简单处理就行编码格式选QRCode把设备编号作为内容前端用uni.scanCode扫码后解析整个链路没有技术门槛但实际使用体验会好非常多。