我们实验室有一台服务器电源报警,微信群里的接龙报修三天后才被人翻到。这种场景在机房运维里太常见了——报修信息散落在聊天记录、电话和口口相传里,没有入口、没有单号、没有处理人、更没有闭环。后来我干脆做了一个组合方案:微信小程序 + PHP + uniapp 的机房设备故障报修平台。整套系统用 uniapp 开发微信小程序端,后端用 PHP 提供接口,MySQL 存数据,覆盖从提交报修、接单处理到完成验收的完整流程。如果你正在做课设/毕设,或者学校、小企业内部确实需要一套轻量报修工具,这篇文章应该能帮上忙。我会把需求梳理、数据表结构、后端接口逻辑、小程序端写法,以及联调阶段踩过的坑都串一遍,尽量做到你照着能自己搭起来。
1. 先想清楚需求再动手:报修平台到底要解决什么问题
1.1 我见过的机房报修混乱现场
做这个系统之前,我在几个不同场景里观察过报修流程。无论是学校机房、实验室还是小公司的设备间,报修痛点高度一致。
最典型的情况是:设备故障后,使用者在微信群里喊一句“XX机房第三排电脑开不了机”,然后就没有然后了。运气好遇到管理员刚好看到消息,回复一句“收到”,运气不好消息直接被闲聊刷过去。就算真的有人去修,整个过程也没有任何结构化记录——谁报修的、什么设备、什么故障现象、谁接单、几点到场、换了什么配件、最后怎么解决的,全是靠脑子记。等月底想统计一下哪种设备故障率最高,发现根本没有数据可查。
还有一些更隐蔽的问题:维修工到了现场才发现用户只说了“电脑坏了”,但没说是蓝屏、不开机还是网络不通,导致来回跑第二趟;两个人同时看到了报修消息,结果都去处理,或者都以为对方会处理;维修完成后没有验收环节,用户根本不知道问题到底解决了没有。
这就是我当时建这个平台的核心初衷:把“报修”从即时通讯工具里的口语化消息,变成一个有序流转的业务工单。
1.2 三种角色和对应的功能地图
想要系统不混乱,第一步是把角色拆清楚。这个报修平台我分了三种用户角色:普通报修人、维修工、管理员。
普通报修人是最多的使用者。他们要能登录小程序,选择设备并提交报修单,查看自己提交过的工单状态,在工单还没被受理时允许撤回。维修工负责处理报修。他们要能看到待受理的工单并“抢单”或者被管理员指派,处理过程中可以更新处理记录,故障解决后提交维修结果等待用户确认。管理员承担管理职责。他们维护机房和设备信息,管理维修工账号,强制指派或关闭异常工单,查看整体统计。
三种角色对应的功能,从权限上可以这样划分:
| 功能 | 普通用户 | 维修工 | 管理员 |
|---|---|---|---|
| 提交报修工单 | 支持 | 支持 | 支持 |
| 查看自己提交的工单 | 支持 | 支持 | 支持 |
| 撤回未受理工单 | 支持 | 不支持 | 支持 |
| 待受理工单列表 | 不可见 | 支持 | 支持 |
| 接单/处理工单 | 不可见 | 支持 | 支持 |
| 指派维修工 | 不支持 | 不支持 | 支持 |
| 设备/机房管理 | 不可见 | 不可见 | 支持 |
| 工单统计 | 不可见 | 不可见 | 支持 |
1.3 为什么是“小程序 + uniapp + PHP”,而不是别的
技术选型在当时其实没有太多纠结。
先说前端。小程序是刚需,因为机房使用者不可能为了报故障专门装一个 App,微信扫一扫或者搜一下就能用才是现实的。真正的分歧在于是直接用微信原生小程序开发,还是用 uniapp。我选择 uniapp 的核心原因是:它是 Vue 语法,组件化和状态管理更顺手,而且以后如果学校想要一个 H5 管理端或者打包成 Android 应用,同一套代码可以复用很大一部分。事实证明写起来也确实比原生 WXML 舒服不少。
后端用 PHP 更直接。这个平台的并发量不会很高,日常同时在线可能就几十个人,PHP 的架构完全足够支撑,而且部署成本极低。一台轻量服务器装上 Nginx + PHP 环境就能跑,本地调试用 phpStudy 或 XAMPP 也毫无压力。对于毕设、课设或者内部小工具来说,PHP 的学习和维护门槛也比 Java、Go 要低。
这套组合的核心逻辑是:用最少的成本把业务跑通,同时给以后扩展预留空间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据表这样设计,工单流转才不容易乱
2.1 用户设备工单附件流转记录,五张表各司其职
报修系统的数据结构核心不是“报修”这两个字,而是“工单的状态变化”。所以我在设计表的时候,除了基础的用户表、设备表、工单表之外,还特意加了附件表和状态流转记录表。数据库名可以直接用你喜欢的项目标识,比如标题里那个 u3em23f1,也可以改成 repair_system,反正连接配置抽到一个 db.php 里,换库名只改一处。
最核心的 repair_order 工单表,字段大概是这样的:
sql复制CREATE TABLE `repair_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL COMMENT '工单号,如BX20250612001',
`device_id` int(11) NOT NULL COMMENT '报修设备ID',
`user_id` int(11) NOT NULL COMMENT '报修人ID,关联user表',
`fault_desc` text NOT NULL COMMENT '故障描述',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0待受理 1处理中 2待确认 3已完成 4已撤销',
`assignee_id` int(11) DEFAULT NULL COMMENT '维修工ID,关联user表',
`assign_time` datetime DEFAULT NULL COMMENT '接单时间',
`finish_remark` text COMMENT '维修结果说明',
`finish_time` datetime DEFAULT NULL COMMENT '维修完成时间',
`create_time` datetime NOT NULL COMMENT '提交时间',
`update_time` datetime NOT NULL COMMENT '最近更新时间',
PRIMARY KEY (`id`),
KEY `idx_device` (`device_id`),
KEY `idx_user` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意几个细节。第一,工单号不要用自增 ID 直接展示给用户,用户看到“工单号 1024”没有任何感知,我采用日期加序号的组合生成方式,比如 BX20250612001,看起来正规也方便在群里沟通。第二,status 字段我用的是数字而不是字符串,原因是数字在代码里比对方便,存数据库也更省空间。第三,assignee_id 和 user_id 都指向 user 表,但含义完全不同,一个代表“报修人”,一个代表“维修处理人”,命名上要区分清楚,避免后面联表查的时候头脑发晕。
user 表在基础字段之外,主要是通过 role 字段区分角色。简单方案可以用 role = 1 表示普通用户、role = 2 表示维修工、role = 3 表示管理员。设备表 device 要记录的字段包括设备编号、名称、型号、所在机房、IP 地址或者 SN 序列号、设备状态等。我这里把“机房”做成 device 表里的 room_name 字段,没有单独建机房表。如果机房数量少,这样确实效率最高。只有当你要做“按机房统计工单量”或者“维修工按机房分组负责”的时候,才有必要单独拆出 room 表。
额外的一张 repair_attachment 附件表很轻量,字段就是 id、order_id、file_url、create_time,用于保存用户上传的故障照片。你也可以把图片 URL 用 JSON 格式直接塞进工单表的 images 字段里。两种我都试过,独立表的好处是以后要扩展“维修前/维修后对比图”时不用改工单表结构。
2.2 状态流转记录表为什么值得单独建
很多第一次做类似系统的人会忽视 order_log 这张表,觉得工单表里已经有 status 字段了,每次变更直接 update 不就行了?
我一开始也是这么想的,直到有一次用户来问:“我这个工单上周四就提交了,怎么现在还没人处理?”我想去查什么时候从“待受理”变成“处理中”的,结果发现工单表里只有当前状态,历史状态被覆盖得干干净净,根本证明不了什么。后来又碰到管理员想统计平均维修时长,发现只能拿到“创建时间”和“完成时间”,中间被谁接过、卡在哪个环节,完全是一笔糊涂账。
所以从第二个版本开始,我加了一张 repair_order_log 表。字段非常简单:id、order_id、from_status、to_status、operator_id、remark、create_time。每次工单状态发生变化,都在业务代码里顺手写一条日志。从“待受理”变成“处理中”,记一条;从“处理中”变成“待确认”,再记一条。这样无论什么时候回查,都能像看流水账一样把整个工单的一生拉出来。
这张表带来的额外价值出乎意料:管理员统计“某位维修工平均处理时长”时,可以直接按 assignee_id 分组,用处理完成时间减去接单时间;如果要看某个时段提交的工单里有多少在 24 小时内被受理,也可以从这张日志表里精确算出每个环节的停留时间。这些数据才是运维管理真正关心的东西。
2.3 设备列表是从哪来的
工单表里需要 device_id,那设备数据总得有人维护。这个坑我替你先踩了:不要试图让报修用户自己填写设备名称,你想想,用户在紧急情况下根本不会去查设备编号,他只会告诉你“进门第三排靠窗那台电脑坏了”。
所以设备数据需要管理员提前维护进去。机房里的每台电脑、交换机、空调、服务器,都在 device 表里有一条记录,字段大概是设备编号、位置描述、负责人。报修的时候,用户在小程序端通过选择器选择设备,不需要手动输入。
设备表我用的核心结构大致如下:
sql复制CREATE TABLE `device` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`device_no` varchar(32) NOT NULL COMMENT '设备编号',
`device_name` varchar(64) NOT NULL COMMENT '设备名称',
`room_name` varchar(64) NOT NULL COMMENT '所在位置/机房',
`device_type` varchar(32) DEFAULT NULL COMMENT '类型:电脑/空调/服务器等',
`status` tinyint(1) DEFAULT '0' COMMENT '0正常 1维修中 2停用',
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_device_no` (`device_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
管理员在后续维护中要注意:设备状态是“维修中”时,前端最好不要让用户继续选择这台设备提交报修,否则容易出现同一台设备同时挂好几个工单的情况。
3. PHP 接口层的实现重点:登录鉴权、状态流转、通知推送
3.1 先把接口目录和返回格式统一起来
后端代码如果散乱,联调阶段会非常痛苦。我做这套 PHP 后端时,目录结构一开始就定得比较简单清晰:
text复制project_root/
├── api/ # 前端调用的所有接口入口
│ ├── login.php # 微信登录
│ ├── upload.php # 图片上传
│ ├── order.php # 工单相关接口
│ └── device.php # 设备相关接口
├── include/
│ ├── db.php # PDO 数据库连接
│ ├── response.php # 统一返回
│ ├── auth.php # token 鉴权
│ └── wxapi.php # 微信接口封装
├── uploads/ # 上传的图片目录
└── repair.sql # 数据库初始化脚本
我个人强烈建议,所有接口不管成功还是失败,都返回同一个 JSON 结构,前端解析起来才不用到处判断。约定如下:
php复制{
"code": 0, // 0 表示成功,非 0 表示业务错误
"msg": "ok",
"data": {}
}
我写了一个公共函数放在 include/response.php 里:
php复制<?php
function jsonOut($code = 0, $msg = 'ok', $data = null) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'code' => $code,
'msg' => $msg,
'data' => $data
], JSON_UNESCAPED_UNICODE);
exit;
}
这里有一个容易忽略的点:一定要用 JSON_UNESCAPED_UNICODE,否则中文会变成 \uXXXX 的转义串,虽然前端也能解析,但你在浏览器里直接调试接口时满屏转义字符非常影响排查效率。
3.2 登录流程:code 换 openid,再换自己的 token
小程序的登录和传统网页登录完全不同。你没法在微信小程序里输入用户名密码,因为微信生态的信任体系是建立在 openid 上的。用户打开小程序后,前端调用 wx.login 拿到一个临时 code,然后把 code 传给后端。后端拿这个 code 加上小程序的 appid 和 secret,去微信服务器换 openid 和 session_key。
关键逻辑在 include/wxapi.php 里:
php复制<?php
function code2openid($code) {
$appid = '你的小程序appid';
$secret = '你的小程序secret';
$url = "https://api.weixin.qq.com/sns/jscode2session?appid={$appid}&secret={$secret}&js_code={$code}&grant_type=authorization_code";
$resp = file_get_contents($url);
$wx = json_decode($resp, true);
if (!isset($wx['openid'])) {
return null;
}
return $wx['openid'];
}
这里要注意:secret 是后端机密信息,绝对不能写进小程序前端代码里。如果哪天你在前端代码包里看到了自己的 appsecret,相当于把你的小程序控制权交出去了。
拿到 openid 之后,去 user 表查这个用户是否存在,不存在就自动创建一个普通用户,存在就直接使用。随后我为用户生成一个随机 token,把他存到表的 token 字段里,返回给前端。前端后续所有请求都在 header 里带 token。token 的好处是后端不用维护 session,天然适合这种无状态接口。
在 include/auth.php 里提供一个获取当前登录用户的方法:
php复制<?php
function getCurrentUser($pdo) {
$token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!$token) {
jsonOut(401, '未登录');
}
$user = $pdo->prepare("SELECT * FROM user WHERE token = ?");
$user->execute([$token]);
$info = $user->fetch();
if (!$info) {
jsonOut(401, '登录已过期');
}
return $info;
}
登录态的保持上有个小经验:token 的有效期可以设得长一点,比如 30 天,因为普通用户可能一个星期才打开一次小程序,如果每次打开都要重新登录,体验会很差。如果你做的是对安全性要求更高的项目,再考虑 refresh token 机制也不迟。
3.3 工单状态机:不要让人随便改状态
工单表的核心是 status,但状态之间的跳转不应该是“想改就改”。系统里我明确限制了状态流转路径,只有以下几种情况是合法的:
| 操作 | 状态变化 | 说明 |
|---|---|---|
| 用户提交报修 | null -> 0 | 新建工单,初始为待受理 |
| 维修工接单 | 0 -> 1 | 待受理变为处理中 |
| 维修工提交结果 | 1 -> 2 | 处理中变为待确认 |
| 用户确认完成 | 2 -> 3 | 待确认变为已完成 |
| 用户撤回 | 0 -> 4 | 只有待受理状态能撤回 |
| 管理员强制关闭 | 任意 -> 4 | 处理异常工单 |
对应到 PHP 代码里,接单接口就是核心。这里牵扯到一个非常经典的并发问题。按理说多个维修工同时看到一张待受理工单,谁先点“接单”谁就该抢到。但是如果后端代码先 SELECT 一下看 status 是不是 0,再 UPDATE,那就完蛋了——两个人同时 SELECT 都看到 status = 0,然后都执行 UPDATE,最后工单会被后更新的那个人覆盖。
正确的写法是用一条 UPDATE 语句搞定判断和更新:
php复制<?php
$stmt = $pdo->prepare(
"UPDATE repair_order
SET assignee_id = :uid,
assign_time = NOW(),
status = 1,
update_time = NOW()
WHERE id = :id
AND status = 0
AND assignee_id IS NULL"
);
$stmt->execute(['uid' => $currentUser['id'], 'id' => $_POST['order_id']]);
if ($stmt->rowCount() === 0) {
jsonOut(4001, '手慢了,工单已被其他维修工接走');
}
看到没,UPDATE 自带条件,如果影响行数是 0,说明工单已经不是待受理状态了。数据库的行锁会保证同时只有一个维修工的 UPDATE 生效,这种方案不需要锁表,性能也好。这是我在这个项目里最想分享的一个实战经验。
3.4 订阅消息:用户不主动授权,事后就没法通知
工单状态变化了,怎么让报修人知道?微信小程序早就下线了模板消息,现在用的是订阅消息。订阅消息一个很坑的机制是:用户必须主动在弹窗里点“允许”,你才能给他推送一次消息,而且一次授权只能推送一条。用户如果点了“总是保持以上选择,不再询问”,也仅仅是以后弹窗不再出现,订阅次数依然是每次需要重新请求。
所以我在代码里的策略是:在用户点击“提交报修”按钮之前,先调用小程序的 wx.requestSubscribeMessage,让用户先订阅“报修进度通知”这个模板;等工单状态流转时,后端再调用微信的 subscribeMessage.send 下发真正的内容。
PHP 端发送订阅消息需要先获取 access_token,然后调用 send 接口。access_token 要缓存,不要每次都请求,微信的 access_token 有效期是 7200 秒,而且每天获取次数有限。我在 include/wxapi.php 里做了简单缓存:
php复制<?php
function getAccessToken() {
$cacheFile = __DIR__ . '/access_token.json';
if (file_exists($cacheFile)) {
$cache = json_decode(file_get_contents($cacheFile), true);
if ($cache['expire_time'] > time()) {
return $cache['access_token'];
}
}
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
$resp = json_decode(file_get_contents($url), true);
file_put_contents($cacheFile, json_encode([
'access_token' => $resp['access_token'],
'expire_time' => time() + 7000
]));
return $resp['access_token'];
}
发送订阅消息的核心逻辑,就是把模板 ID、接收人 openid、具体数据填好,POST 到指定接口,如果返回 errcode 为 0 就是成功。如果遇到用户没有订阅就发送,会提示“43101 user refuse to accept the msg”,这时候不要慌,前端已经引导过订阅,仍然拒绝的用户收不到也是符合平台规则的。
4. uniapp 小程序端:报修页面怎么写才顺手
4.1 页面结构尽量少,核心流程要直接
uniapp 里的页面结构,我第一版设计得很复杂,又是首页展示大屏又是广告位,后来发现完全跑偏。对于报修工具,用户核心诉求是两件事:发起报修、查看进度。所以最终只保留了几个必要页面:
- pages/index/index,展示简单欢迎信息和常用功能入口
- pages/repair/add,提交报修单,这是整个系统最核心的页面
- pages/order/list,工单列表,区分用户视角和维修工视角
- pages/order/detail,工单详情和状态流转操作
- pages/device/list,设备展示页面,管理员可新增设备
- pages/user/index,我的页面,展示个人资料和登录状态
pages.json 里注册 tabBar 的时候,我用了三个主 tab:首页、报修、我的。因为对于高频使用工具,把“报修”按钮放在正中间,用户进来一眼就能看见,比放在二级页面里要高效得多。
4.2 登录态处理:封装一个带 token 的 request
uniapp 开发小程序时,不建议每个页面直接调 uni.request,而是封装一个公共请求方法。我建了 utils/request.js:
javascript复制const BASE_URL = 'https://yourdomain.com/api';
export function request(path, data = {}, method = 'POST') {
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + path,
data,
method,
header: {
'Content-Type': 'application/json',
'Authorization': uni.getStorageSync('token')
},
success: (res) => {
if (res.data.code === 401) {
// token 过期,跳转登录
uni.navigateTo({ url: '/pages/user/login' });
reject(res.data);
return;
}
resolve(res.data);
},
fail: (err) => reject(err)
});
});
}
在首页或者 App.vue 的 onLaunch 生命周期里做静默登录。注意,小程序里 wx.login 是可以静默完成的,不需要用户点任何授权按钮:
javascript复制uni.login({
provider: 'weixin',
success: (loginRes) => {
// 把 loginRes.code 发给后端换 token
request('/login.php', { code: loginRes.code }).then((res) => {
uni.setStorageSync('token', res.data.token);
uni.setStorageSync('userInfo', res.data.userInfo);
});
}
});
这就是为什么用户第一次打开小程序时几乎感受不到登录过程,直接就能用。一定要避免做手机号授权弹窗强制绑定,微信现在对手机号授权限制很严格,而且报修流程根本不需要手机号,用户提交的工单里有设备位置和联系方式字段就够了。
4.3 报修表单:设备选择器、故障描述、图片上传一次说清
报修页面的表单,字段设计也要克制。我最终只保留了:故障设备(选择器)、故障描述(多行文本)、图片上传(最多三张)、联系人手机号。再多的字段都会降低用户提交意愿。
设备选择用 picker 组件。进入页面时先拉取设备列表:
javascript复制request('/device.php?action=list').then((res) => {
this.deviceList = res.data;
});
然后表单里用 picker 展示:
html复制<picker mode="selector" :range="deviceList" range-key="device_name" @change="onDeviceChange">
<view>{{ selectedDevice ? selectedDevice.device_name : '请选择故障设备' }}</view>
</picker>
故障描述那里我写了一个 textarea,并给出 placeholder 引导用户写清楚现象,例如“开机后风扇狂转、屏幕无信号”“网络端口插上后灯不亮”。用户描述得越清楚,维修工带对工具的概率就越高。
图片上传的核心逻辑如下:
javascript复制uni.chooseImage({
count: 3,
success: (res) => {
res.tempFilePaths.forEach((filePath) => {
uni.uploadFile({
url: BASE_URL + '/upload.php',
filePath: filePath,
name: 'file',
header: {
'Authorization': uni.getStorageSync('token')
},
success: (uploadRes) => {
// 注意:uploadFile 返回的数据在 H5 和 App 上类型可能不一样,小程序端通常需要 JSON.parse
const data = JSON.parse(uploadRes.data);
this.imageList.push(data.data.url);
}
});
});
}
});
传完图片,把图片 URL 数组和表单数据一起 POST 到 order.php 的 save 接口。提交按钮一定要放一个“防止重复点击”的开关,因为用户如果手机卡顿点两次,就会生成两张一模一样的工单。最简单方案是提交前把按钮 disabled,请求结束再恢复,或者通过 order_no 也能判断弱网重试的场景。
4.4 工单列表和详情:用状态驱动页面按钮
工单列表页是用户最喜欢停留的地方。为了不让用户觉得查询起来费劲,我在列表页顶部放了几个筛选 tab:全部、待受理、处理中、已完成。每次切换重新拉接口。
工单详情页的按钮并不是固定的,而是要跟随 status 动态变化。这个需求我在开发第三版时才开始做,原因是前两版把用户和维修工的操作都堆在同一版,代码越写越乱。后来我把详情页拆成了三块状态区:
待受理状态下,用户能看到“撤回工单”按钮,维修工看不到这个按钮;处理中状态下,用户看到当前维修工和处理进度,维修工看到“填写维修结果”;待确认状态下,用户看到“确认完成”按钮,维修工看到“等待用户确认”的提示;已完成状态则只展示整个流程的时间线。
代码实现上,只需要在 data 里维护一个 statusMap,根据当前 user.role 和 order.status 计算要显示的按钮数组。这个做法比在模板里堆一堆 v-if 清晰得多。
5. 联调阶段踩过的坑:开发者工具、域名配置和页面适配
5.1 HBuilderX 运行到微信开发者工具,提示“不是开发者”
这个报错很多人第一次遇到都会懵。HBuilderX 本身不校验开发者身份,微信开发者工具打开项目时会校验当前登录的微信号有没有权限操作这个 AppID。
解决办法分两种。如果这个 AppID 是你自己注册的、可正常登录的小程序,那就去微信公众平台里把当前微信号加入项目开发者。具体路径是:登录微信公众平台 -> 成员管理 -> 项目成员 -> 添加成员。添加完,重新打开微信开发者工具即可。
如果只是为了本地联调,不耐烦去公众平台加成员,可以直接在微信开发者工具里勾选“使用测试号”或者自己申请一个小程序测试号。测试号不受 appid 权限限制,但很多真实接口能力也受限,比如订阅消息、支付这些都需要正式 AppID 才能体验完整。
我当时卡了半天,最后发现只是微信开发者工具登录的账号不是管理员账号,换账号登录就正常了。所以遇到这个报错第一反应应该是:检查开发者工具右上角登录的微信,到底是不是小程序项目的管理员或项目成员。
5.2 真机请求全部失败,合法域名配置必须提前做
用 HBuilderX 运行微信小程序时,默认的请求域名校验是非常严格的。你在开发者工具里调试时还能正常请求后端,但是一到真机预览,所有请求都可能直接报 fail,提示“url not in domain list”。
原因是微信要求小程序请求的接口域名必须在小程序后台配置为合法域名,而且必须是 HTTPS。我本地开发用的是 http://localhost,根本不在合法域名列表里,所以必挂。
解决办法是分两步。开发阶段,可以在微信开发者工具的“详情 -> 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,这样本地就能用 http 接口了。真机预览前,在手机上打开小程序开发版的调试模式,也能临时绕过这个校验。
但一旦要发布正式版,就必须把线上后端代码部署到一台能用 HTTPS 域名访问的服务器,然后在微信公众平台的“开发管理 -> 开发设置 -> 服务器域名”里配置。要注意的是,request 合法域名和 uploadFile 合法域名是分开配置的,图片上传的域名如果和接口域名不同,两边都要填。
5.3 改了 AppID 但微信开发者工具里还是旧 ID
搜索热词里有个问题很典型:“为什么运行到微信小程序模拟器中,小程序id还是原来的”。这个问题我也遇到过。原因在于,在 HBuilderX 中,开发者通常只修改了 manifest.json 的 uni-app 应用标识,而没有同步修改微信小程序的专用配置。
正确的做法是:在 HBuilderX 里找到 manifest.json,进入“微信小程序配置”面板,把微信小程序的 AppID 填进 mp-weixin.appid 字段。如果你直接改源码,就是修改 manifest.json 里的如下片段:
json复制{
"mp-weixin": {
"appid": "你的新AppID",
"setting": {
"urlCheck": false
},
"usingComponents": true
}
}
改完以后,一定要重新编译运行。HBuilderX 运行到微信开发者工具时会自动生成微信开发者工具的项目文件,如果旧 AppID 还在 project.config.json 里,可能因为编译缓存没有刷新。遇到这种情况,先停掉运行,删掉项目目录下的 unpackage/dist/dev/mp-weixin 文件夹,再重新运行,基本都能解决。
5.4 软键盘遮挡输入框和顶部导航栏高度适配
在 uniapp 写小程序时,发现最影响体验的适配问题是:在报修页面填写故障描述时,软键盘弹起来会把 textarea 挡得严严实实。后来发现小程序原生页面的输入框本身有 adjust-position 属性,默认情况下键盘弹起会调整页面位置,但如果页面里有自定义固定底部或者自定义导航栏,键盘处理就会出问题。
我的解决办法是给 textarea 设置 cursor-spacing 属性,指定光标与键盘之间的距离,同时不用自定义底部留白,让页面滚动区自然撑开:
html复制<textarea
v-model="faultDesc"
placeholder="请描述故障现象"
:cursor-spacing="20"
maxlength="200"
/>
顶部导航栏的适配也是 uni-app 开发中绕不开的问题。如果用了自定义导航栏,就没有免费的午餐。状态栏高度、胶囊按钮高度在不同手机上都不相同,不能写死。可以通过 uni.getSystemInfoSync 和 uni.getMenuButtonBoundingClientRect 获取真实数据:
javascript复制const sysInfo = uni.getSystemInfoSync();
const menuRect = uni.getMenuButtonBoundingClientRect();
// statusBarHeight:状态栏高度
// menuRect.top:胶囊按钮顶部距离屏幕顶部距离
// 导航栏高度 = (menuRect.top - statusBarHeight) * 2 + menuRect.height
如果你不想维护自定义导航栏,最开始还是优先使用微信原生导航栏,把页面标题写在 pages.json 的 navigationBarTitleText 配置里,能省掉一大堆兼容问题。
6. 上线前后最容易翻车的三个隐蔽点
6.1 图片上传成功,数据库却存了没用的临时路径
有段时间测试反馈:报修单详情页里图片加载不出来。我打开数据库一看,发现 repair_attachment 表里存的图片地址全都是 wxfile:// 开头的临时路径。问题的根源在 uniapp 的 uni.chooseImage 成功后返回的 tempFilePaths 只是本地临时文件路径,只有你通过 uni.uploadFile 上传到后端,后端保存到你的 uploads 目录后,那个路径才有真正的意义。
正确流程是:第一步 chooseImage 选图,第二步 uploadFile 把图片传到 PHP 服务器,PHP 端接收文件后保存到 uploads 目录,并把可访问的 URL 返回给前端,第三步前端把返回的 URL 随工单一起提交。不能直接把 tempFilePath 塞进业务数据里。
排查这类问题时,可以先在浏览器开发者工具 Network 面板看 uploadFile 请求是否成功,再看后端 uploads 目录里是否真的多出文件,最后再看数据库里存的 URL 是相对路径还是完整 URL。三步基本能定位。
6.2 两张维修工都显示“待处理”,点开却是同一张单
这个问题在第 3 章提到过,但真实场景下往往不是技术问题,而是需求问题。产品上你希望多个维修工都能看到“待处理”工单,让手快的人优先接单,这没问题。代码上最关键的是,所有“抢占”语义的操作都必须走服务端原子更新,而不是先查询判断再更新。
我当时第一次写抢单逻辑时是这么写的:
php复制// 错误示范
$order = $pdo->query("SELECT * FROM repair_order WHERE id = {$id}")->fetch();
if ($order['status'] == 0) {
$pdo->exec("UPDATE repair_order SET status = 1 WHERE id = {$id}");
}
这段代码在单用户测试时完全正常,但两台手机同时操作就穿帮了。后来改成一条 UPDATE + rowCount 判断后,再没有出现过重复接单问题。这里我总结出一条规律:在工单、订单这类存在“唯一处理人”语义的系统里,永远不要相信先读后写,宁可多写几条更新条件,也要确保服务端的判断和修改在一个原子操作内完成。
6.3 订阅消息失效:用户在提交前没有完成订阅授权
订阅消息是报修平台通知用户的核心通道,但它的失败往往不是后端代码问题,而是前端交互链路设计问题。小程序规定,requestSubscribeMessage 必须由用户点击行为直接触发,不能在页面加载完成后自动弹窗,也不能在回调里嵌套调用。也就是说,你不能在提交报修成功后再去请求订阅,因为那时弹窗已经脱离了用户点击的上下文,微信会直接拒绝甚至不弹窗。
正确做法是:用户点击“提交报修”按钮之后,先弹出 wx.requestSubscribeMessage 的授权请求,等用户选择允许后,再调用后端接口真正创建工单。这样既符合微信的交互要求,也能保证后端在需要发送通知时,用户已经订阅过模板消息。
如果确实遇到了用户没有授权订阅的情况,备选方案是在工单详情页显示状态变化,用户只要打开小程序就能看到。这个兜底方案在真实使用中反而比推送更可靠,毕竟报修人员通常会主动查看进度。
7. 结项前的自测清单,以及这套代码还能往哪里成长
7.1 几个必须反复回测的流程用例
项目要交付或者上线之前,我强烈建议把这个清单完整跑几遍,而不是只测试“提交报修成功”这个快乐路径。下面这些用例都是我实际遇到问题后总结出来的:
| 测试用例 | 操作路径 | 预期结果 |
|---|---|---|
| 新用户进系统 | 首次打开小程序自动登录 | 能正常创建用户并进入首页 |
| 正常报修 | 选择设备+填写描述+上传图片+提交 | 生成 |
