简介:这份压缩包面向IT运维人员、设备管理员与ASP初学者,提供一套以二维码为入口的设备报修管理源码,用于解决传统报修中设备编号难记、故障信息填写繁琐、维修进度难以追踪的问题。包内共121个文件,约205KB,以19个asp页面文件为核心业务逻辑,涵盖报修提交、报修列表、派工、维修登记、评价与统计报表等模块;另有9个js脚本、8个css样式与45个gif、32个png界面素材共同构成前后端页面,并附带bat配置脚本、mdb与db数据库文件及dll组件,便于直接部署调试。资源已有1671人学习下载,读者可从中了解设备二维码生成、扫码报修、工单派发与进度追踪的完整流程,以及微信端报修集成和故障频率、维修耗时等数据统计思路,也可参考后台派工与评价模块的表单组织方式,作为课程设计或小型运维系统的改造基础,体验版适合先熟悉整体结构再做定制。
1. 设备坏了靠电话和群里喊,报修记录最后落在谁的相册里
设备坏了,报修靠电话和群里喊一嗓子,维修记录最后落在某个人手机相册里,这是很多机房、实验室、园区运维的真实状态。二维码报修系统源码这类资源的价值,就是把这套流程固定成一条可查的链路:设备贴码、扫码填单、后台派单、完工归档。它不是通用 OA,也不需要一整套微服务,一个前后端一体的管理后台加一个免登录的 H5 报修页,跑在一台 2 核 4G 的机器上就够用。适合谁?手里有几十到几千台设备要管、又不打算买 SaaS 的中小团队;也适合想拿一套完整业务闭环练手的开发者,工单状态机、扫码免登录、图片上传这三块,都是能直接搬进别的项目里的重点模块。
2. 解包与选型:目录结构、运行依赖和三处必改配置
2.1 解压阶段先处理文件名乱码和嵌套压缩包
这类源码包多数是在 Windows 上打包的,在 Linux 或 macOS 上直接解,中文目录名和带注释的 SQL 文件很容易变成一串问号。我第一次解这类包时,前端静态目录名直接乱码,后面 Nginx 怎么配都是 404,排查了半小时才发现问题出在解压阶段,而不是部署。
# 1) 解到独立目录,不要直接解在下载目录里,后面清理方便 mkdir -p ~/work/repair-src && cd ~/work/repair-src # 2) Linux 下解 rar。unar 对 GBK 文件名更友好,-e 指定编码 unar -e GBK ~/Downloads/二维码报修系统源码.rar # 3) 没有 unar 就用 7z,-mcp=936 是简体中文代码页 # 7z x -mcp=936 ~/Downloads/二维码报修系统源码.rar -o./ # 4) 解完先看体积分布和层级,重点找嵌套压缩包和大目录 du -sh ./* | sort -h find . -maxdepth 3 \( -name "*.rar" -o -name "*.zip" -o -name "*.7z" \) -print find . -maxdepth 4 \( -name "*.sql" -o -name "pom.xml" -o -name "package.json" -o -name "*.yml" \) -print第一步单独建目录,是为了后面能整体删掉重来,源码包跑不起来时“重解一次”往往比修环境快。第二步的-e GBK是关键,不加这个参数,压缩包里 GBK 编码的文件名会被当成 UTF-8 解码,目录名直接损坏。第三步给 7z 用户的备选方案,-mcp=936同理。第四步在动手改代码前先把结构弄清楚,尤其是嵌套的 rar,很多包把前端工程单独压了一层,不解开就会误以为“没有前端源码”。
解完之后对照下表过一遍,心里就有了整体结构:
| 目录或文件 | 通常是什么 | 先确认什么 |
|---|---|---|
sql/、db/、*.sql | 建库脚本 | 字符集是否 utf8mb4、脚本里有没有 DROP DATABASE |
pom.xml、build.gradle | 后端依赖 | java.version、框架大版本、有没有私服仓库地址 |
application*.yml/properties | 后端配置 | 端口、数据源、上传路径、二维码域名前缀 |
static/、public/ | 打包进后端的页面 | 是源码还是已编译产物 |
web/、vue/、front/ | 独立前端工程 | package.json里的 scripts 和依赖版本 |
upload/、files/、doc/ | 运行期目录与文档 | 部署文档是否与实际配置一致 |
node_modules/、.git/ | 打包时误带的垃圾 | 直接删,体积能少一大半 |
2.2 运行依赖:别照文档装,照工程文件装
随包附带的部署文档经常是作者本机环境的快照,版本号未必和代码对得上。我一般不看文档先看工程文件:后端看pom.xml里的java.version和父 POM 的框架大版本,前端看package.json的engines和dependencies,这两处才是准的。
| 组件 | 判断依据 | 常见冲突点 |
|---|---|---|
| JDK | pom.xml的java.version、maven.compiler.source | 8 写的代码用 17 编译,反射和javax包容易失效 |
| MySQL | SQL 脚本头部的版本注释、驱动依赖 | 5.7 导出的脚本在 8.0 上因排序规则和零日期报错 |
| 缓存 | 配置里的 redis 段、代码里的RedisTemplate | 代码没用到却强依赖,白搭一个启动项 |
| Node | package.json的engines | 新版本 Node 的 OpenSSL 与老构建工具不兼容 |
| Web 服务器 | 文档或部署脚本 | 上传大小限制、静态资源路径、反向代理头 |
MySQL 这块有具体的注意点:8.0 的驱动类名是com.mysql.cj.jdbc.Driver,连接串要带serverTimezone,否则时间字段可能整体偏 8 小时;如果报Public Key Retrieval is not allowed,加allowPublicKeyRetrieval=true;如果脚本里出现utf8mb4_0900_ai_ci而目标是 5.7,直接全局替换成utf8mb4_general_ci再导。
缓存的判断更简单,全局搜一下RedisTemplate、@Cacheable、Jedis,一处都没有就把配置里的 redis 段整体注释掉,少一个启动依赖,排错面小一圈。
2.3 建库、导数据与三处必改配置
建库这一步不要省事,字符集一步到位,否则后面设备名里的中文会变成问号,而且是写进数据里的,改起来很麻烦。
-- 建库与专用账号,字符集必须和连接串一致 CREATE DATABASE repair_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'repair'@'%' IDENTIFIED BY 'ChangeMe_2024'; GRANT ALL PRIVILEGES ON repair_db.* TO 'repair'@'%'; FLUSH PRIVILEGALES; -- 导入前先 grep 一下脚本,看到 DROP DATABASE 就把库名改掉再执行 -- mysql -urepair -p repair_db < sql/repair_db.sql上面 SQL 里最后一行FLUSH PRIVILEGES写错了会直接报语法错误,正确写法是FLUSH PRIVILEGES;,这种低级错误在抄别人脚本时很常见,遇到别慌。导入前那句 grep 是血泪经验:有些脚本开头带DROP DATABASE IF EXISTS,你在一个有数据的库上跑,数据就没了。
后端配置里有三处必须改,改完基本就能起:
server: port: 8080 # 前端 baseURL 要和这里对齐,改端口前端必须同步 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/repair_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: repair password: ChangeMe_2024 servlet: multipart: max-file-size: 5MB # 现场照片默认 1MB 往往不够,手机直出就超 max-request-size: 20MB repair: upload-path: /data/repair/upload # 必须是绝对路径,且启动用户有写权限 qr-base-url: https://repair.your-domain.com/r # 二维码里拼的地址前缀,换域名只改这里三处必改分别是数据源、上传路径、二维码域名前缀。数据源里serverTimezone=Asia/Shanghai不要漏,工单的“超时提醒”全靠时间比较,时区差 8 小时会让提醒逻辑彻底失效。上传路径不要用相对路径,相对路径依赖启动时的工作目录,用 systemd 托管时工作目录常常不是你以为的那个。qr-base-url是这份源码里最值得保留的设计,把域名抽成配置项,二维码生成逻辑就不用改代码,后面第 5 章会讲为什么这一条能救命。
前端工程如果独立存在,还要改一处baseURL或VITE_API_BASE,指向后端地址;如果前端被打包进了后端的static目录,确认访问路径是根路径还是子路径,这决定了后面 Nginx 怎么配。
3. 二维码与设备绑定:编码内容、批量生成和扫码入口
3.1 二维码里放什么:裸 ID、完整 URL 还是带签名的短码
这是整套系统里最容易被忽略、但返工成本最高的一个决定,因为码一旦打印贴出去,再改就要重新贴一遍。
| 方案 | 内容示例 | 优点 | 问题 |
|---|---|---|---|
| 裸设备 ID | 1024 | 短、生成简单 | 扫码后还要人工选设备,等于白贴 |
| 明文完整 URL | https://host/r/1024 | 扫码直达 | ID 可枚举,换域名全废 |
| URL + 不可枚举短码 | https://host/r/A7K2M9 | 扫码直达、不可猜 | 需要一张短码映射表 |
| URL + 短码 + 签名 | https://host/r/A7K2M9?s=9f2c... | 防篡改、可溯源 | 生成和校验都多一步 |
我的选择是第三种,短码用 6 到 8 位大写字母加数字,排除容易混淆的0/O、1/I,在设备入库时生成并落库。签名那一层只有在内网之外暴露时才值得加,具体做法是HMAC(短码 + 密钥)取前 8 位放查询参数,扫码入口验一次,成本很低。
短码不可枚举这一点别省。用自增 ID 做路径,任何人都能顺着/r/1、/r/2一路扫下去,把你所有设备的位置和报修记录翻一遍,这不是安全问题,是常识问题。
3.2 批量生成:从设备表到可打印的 PNG
设备上了规模就不可能一张张在线生成,写个脚本从 CSV 批量生成,输出按短码命名,打印店直接按文件名排版。
import csv, os, qrcode from qrcode.constants import ERROR_CORRECT_Q from PIL import Image, ImageDraw BASE = "https://repair.your-domain.com/r" # 必须与后端 qr-base-url 完全一致 OUT_DIR = "./qr_out" os.makedirs(OUT_DIR, exist_ok=True) def make_qr(text: str, path: str, box: int = 10, border: int = 4): qr = qrcode.QRCode( version=None, # 让库按内容长度自动选版本 error_correction=ERROR_CORRECT_Q, # Q 级容错 25%,油污环境够用 box_size=box, # 每模块像素数,决定成品分辨率 border=border, # 白边,规范要求至少 4 个模块,别省 ) qr.add_data(text) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white").convert("RGB") # 下方留一条白边写短码,肉眼看码也能对上设备 canvas = Image.new("RGB", (img.width, img.height + 56), "white") canvas.paste(img, (0, 0)) ImageDraw.Draw(canvas).text((8, img.height + 12), text.rsplit("/", 1)[-1], fill="black") canvas.save(path, "PNG", dpi=(300, 300)) with open("devices.csv", newline="", encoding="utf-8") as f: for row in csv.DictReader(f): # 表头:code,name,location make_qr(f"{BASE}/{row['code']}", f"{OUT_DIR}/{row['code']}.png")box_size=10配dpi=300是反复调出来的组合:300dpi 下 1 毫米约等于 11.8 像素,10 像素的模块大约 0.85 毫米,最终成品的边长等于“模块数 × 10 像素”,再换算成毫米。内容越短模块数越少,成品越小,所以短码比长 URL 更适合小尺寸标签。
border=4是白边(静区)要求,很多人为了省版面把它设成 1 甚至 0,贴到深色设备上就扫不出来,这类问题在现场排查时极难定位,因为屏幕上看着是好的。生成完抽几张用手机扫一下,别等打印完再发现。
3.3 扫码入口:免登录不等于无校验
扫码的人大概率不是系统用户,让他注册登录再报修,转化率基本为零。所以入口必须免登录,但免登录只是不做身份认证,不是不做校验。
@RestController @RequestMapping("/r") public class QrEntryController { @GetMapping("/{code}") public void entry(@PathVariable String code, HttpServletResponse resp) throws IOException { // 1) 短码只做一次映射查询,扫码链路上不要放重业务逻辑 Device d = deviceService.findByShortCode(code); if (d == null || d.getEnabled() == 0) { resp.sendRedirect("/h5/device-offline.html"); return; } // 2) 签发一张只能提交一次报修的单据票据,30 分钟有效 String ticket = ticketService.issue(code, 30); // 3) 跳 H5,票据放 query,提交时回传,后端验签即可 resp.sendRedirect("/h5/report.html?code=" + code + "&ticket=" + ticket); } }第一处的判断要包含“设备停用”,设备报废或搬走后如果码还留着,用户扫码能提交工单,后台就得反复关闭无效单。第二处的票据我一般用短码 + 时间戳 + HMAC拼,服务端无状态校验,不依赖会话。第三处把票据放 query 有日志泄露风险,介意的话改成先写一个短时 cookie 再跳转,实现成本差不多。
限流放在网关或入口处,同一个 IP 每分钟超过 20 次直接拒绝。这条不是防攻击,是防误触,贴着码的机器旁边有人反复扫,日志会多到没法看。
3.4 张贴环节:容错等级、尺寸和材质
| 容错等级 | 冗余比例 | 适用场景 |
|---|---|---|
| L | 约 7% | 屏幕显示、干净纸面 |
| M | 约 15% | 一般室内标签 |
| Q | 约 25% | 机房、车间、有油污和磨损 |
| H | 约 30% | 户外、易脏易刮、需要印 logo |
尺寸上有个粗略公式:成品边长(毫米)≈ 模块数 × 0.5。内容长度 30 字符左右的短码 URL,版本大约在 3 到 4,模块数 29 到 33,边长 15 毫米起步,实际打印建议做到 25 到 30 毫米,留出容错余量。
材质比尺寸更容易翻车。光面铜版纸贴在金属机柜上,灯光一照就是一片白,手机根本对不上焦;换成亚光的不干胶,或者干脆覆一层亚光膜,扫码成功率立刻不一样。弧面、接缝、散热孔旁边都不要贴,贴在平整、干燥、离地 1.2 到 1.6 米的视线高度。贴之前用酒精把表面擦一遍,灰尘是标签最早脱落的原因。
4. 工单状态机:字段设计、抢单并发和超时提醒
4.1 表结构:五个状态、三个时间戳、一个版本号
状态机是整个系统的骨架,字段设计错了,后面所有功能都会别扭。下面是我在这个场景下会用的核心字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键,内部使用 |
order_no | varchar(32) | 对外单号,日期加随机,不要暴露自增 ID |
device_code | varchar(32) | 设备短码,和二维码对应 |
fault_type | tinyint | 故障类型字典,用于统计 |
description | varchar(500) | 文字描述,限制长度 |
images | varchar(1000) | 图片相对路径,逗号分隔或 JSON |
reporter_phone | varchar(20) | 联系号码,用于回访 |
status | tinyint | 0 待受理 / 1 已派单 / 2 处理中 / 3 已完工 / 4 已取消 |
repairer_id | bigint | 处理人,抢单前为 NULL |
create_time | datetime | 提交时间 |
accept_time | datetime | 接单时间,用于算响应时长 |
finish_time | datetime | 完工时间,用于算处理时长 |
remind_count | tinyint | 已提醒次数,做幂等用 |
version | int | 乐观锁,非必需但排查时好用 |
状态值不要用字符串。用PENDING、DONE这类枚举看着清楚,但改一个状态名要动代码、动数据、动前端三处;用数字再配一张字典表或枚举类,成本低得多。时间戳拆成三个而不是一个update_time,是因为响应时长和处理时长是两个考核指标,混在一起就算不出来了。
4.2 抢单并发:一条带条件的更新比加锁省事
多个维修员同时点“接单”,是这类系统上线后第一个暴露的并发问题。常见的错误写法是先select判断repairer_id是否为空,再update写入,两次操作之间有窗口,两个人都会成功。
-- 把“判断空闲”和“写入处理人”合成一条语句,靠行锁保证原子性 UPDATE repair_order SET repairer_id = #{uid}, status = 2, accept_time = NOW(), version = version + 1 WHERE id = #{orderId} AND status = 1 AND repairer_id IS NULL; -- 受影响行数为 1 才算抢到;为 0 说明已被别人接走对应的服务层:
@Transactional public boolean grab(Long orderId, Long uid) { int rows = orderMapper.grab(orderId, uid); if (rows == 0) { return false; // 没抢到是正常业务分支,不要抛异常 } // 抢到之后写日志,这一步不要在持锁期间做远程通知 orderLogMapper.insert(new OrderLog(orderId, uid, "GRAB")); return true; }WHERE里三个条件缺一不可。status = 1保证只有“已派单”的工单能被接,repairer_id IS NULL保证不会覆盖已有的处理人,version + 1让后续的更新能检测到并发修改。
我不用select ... for update的原因很实际:它把锁的持有时间拉长到两次网络往返,QPS 一上来连接池就开始告警;而条件更新的锁只在这一条语句内,粒度小得多。也不要用synchronized,单机部署时看着没问题,一上双实例立刻失效,而且这种 bug 在测试环境永远复现不了。真要上分布式锁,成本比这条 SQL 高一个数量级,这个场景完全不必要。
返回 0 之后前端提示“已被接单,刷新看看”,比弹一个异常栈友好得多。工单列表页刷新频率调高一点,抢单体验就基本够用了。
4.3 超时提醒:定时任务加幂等标记
“30 分钟没人接单就提醒”是这类系统最实用的一个功能,但多实例部署时最容易重复推送。核心思路是:先捞 ID,再用带条件的更新抢占,抢到才推。
@Scheduled(cron = "0 */5 * * * ?") // 每 5 分钟一次,错开整点避免和备份撞车 public void remindPending() { // 一次最多捞 200 条,防止大结果集把内存打满 List<Long> ids = orderMapper.findTimeoutIds(30, 200); for (Long id : ids) { // 带 remind_count < 3 的条件更新,抢到才推,天然幂等 if (orderMapper.markRemind(id) == 1) { notifyService.push(id, "工单超过 30 分钟未受理"); } } }扫表条件大致是status = 1 AND create_time < DATE_SUB(NOW(), INTERVAL 30 MINUTE) AND remind_count < 3,配上create_time的索引,几万条数据量下开销可以忽略。remind_count < 3既是幂等保护,也是防骚扰:一条单子提醒三次还没人处理,问题在流程不在通知。
多实例下更稳妥的做法是把抢占做成UPDATE ... SET remind_count = remind_count + 1 WHERE id = ? AND remind_count < 3,靠数据库行锁串行化,两个实例同时扫也只有个能成功。别用内存里的AtomicBoolean做开关,那是单机思维。
4.4 现场照片:大小、方向与存储命名
现场照片是这类系统里最容易出问题的一环,因为它同时踩三个坑:体积、方向、文件名。
体积上,后端max-file-size已经设了 5MB,但网关层往往还有一道限制。用 Nginx 反代的话,client_max_body_size默认是 1MB,用户传一张手机原图就 413,而后端日志干干净净什么都不打,这种“后端没问题但就是传不上”最容易让人怀疑人生。前端在上传前用 canvas 重绘压到长边 1280,质量 0.8,一张 5MB 的照片能压到 300KB 左右,体验和成本都最优。
方向问题来自 EXIF。安卓和部分机型拍照会在 EXIF 里写Orientation,浏览器一般会按 EXIF 渲染,但服务端一旦重新编码或者生成缩略图,方向就歪了。前端用 canvas 重绘时正好把 EXIF 丢掉、把方向烧进像素,一举两得。
存储命名不要用前端传来的原始文件名:
// 原始文件名带中文和空格,在某些容器和对象存储里会变成 404 的根源 String ext = StringUtils.getFilenameExtension(file.getOriginalFilename()); String name = LocalDate.now() + "/" + UUID.randomUUID() + "." + ext; Path target = Paths.get(uploadPath).resolve(name); Files.createDirectories(target.getParent()); // 按日期分目录,避免单目录文件过多 file.transferTo(target.toFile());按日期分目录不只是为了整洁,单目录文件数超过几万之后,ls和部分文件系统都会明显变慢,备份脚本也更难写。存库时只存相对路径2024-06-01/xxxx.jpg,取的时候拼上配置里的域名前缀,将来换存储时改一个前缀就行,不用动数据。
5. 上线前避坑:五类翻车现场的现象、成因与处置
5.1 后端起不来,日志停在连接池
现象:启动日志打完框架 banner 之后不动了,或者抛Communications link failure,本地同样的包在别人机器上能起。
原因:九成是配置里还留着打包人本机的数据库地址和密码,或者目标 MySQL 是 8.0 而连接串缺allowPublicKeyRetrieval=true;另外一小部分是 8080 端口被占,日志里会有一行Port 8080 was already in use,容易被前面的报错刷过去。
解决:先查端口ss -lntp | grep 8080,再单独验证数据库连通性mysql -h127.0.0.1 -urepair -p -e "select 1",最后对一遍时区select @@global.time_zone, @@session.time_zone。这三步按顺序走,能覆盖 95% 的启动失败。
5.2 扫码后白屏或者 404,本地却正常
现象:本地npm run dev一切正常,部署到服务器后扫码打开是空白页,控制台一堆/assets/index-xxxx.js404。
原因:两个常见成因。一是前端用了 history 路由,Nginx 没做回退,刷新或直接访问子路径就 404;二是前端部署在子路径/h5/下,但构建配置里的 publicPath 还是/,静态资源全指向了根目录。
解决:Nginx 加回退,构建配置改成子路径。
location /h5/ { alias /data/www/repair-h5/; index index.html; # history 路由必须回退到 index.html,否则刷新即 404 try_files $uri $uri/ /h5/index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header X-Real-IP $remote_addr; client_max_body_size 20m; # 与后端 multipart 上限保持一致 }try_files那一行是 history 路由的必需配置,少了它,只有从首页点进去能用,扫码直达就废。client_max_body_size要和后端配置对齐,两边不一致时,小图能传大图失败,很难查。
5.3 提交报修返回 400,图片还丢了
现象:表单填完点提交,接口返回 400,后端日志里是Maximum upload size exceeded或者一堆字段校验错误。
原因:文件超限只是其中一种;更常见的是前后端字段名不一致,前端传deviceCode,后端 DTO 里写的是device_code,反序列化后是 null,校验直接拦掉。日期格式也是高发区,前端传2024-06-01,后端期望yyyy-MM-dd HH:mm:ss。
解决:加一张字段对照表,把前端请求体的 key 和后端 DTO 的字段名逐个核一遍,比对着日志猜快得多。文件超限要同时改后端max-file-size、网关client_max_body_size、前端压缩三处,只改一处等于没改。校验失败时把BindingResult里的错误信息打到日志里,别只返回一个 400。
5.4 同一条工单被两个人接走
现象:两个维修员几乎同时点接单,都提示成功,工单详情里出现两条接单记录。
原因:写成了先查询再更新,两次操作之间有窗口;或者更新语句里漏了status条件,只判断了repairer_id IS NULL。
解决:按 4.2 的条件更新改,一行 SQL 解决。数据库层再加一道兜底,给(order_id, repairer_id)建唯一索引,万一代码回退到旧逻辑,插入直接失败而不是产生脏数据。这类问题不要指望测试环境复现,压测脚本并发打 50 次就能稳定暴露。
5.5 换了域名,贴出去的二维码全部失效
现象:系统从测试域名迁到正式域名,已经贴出去的码扫开打不开,或者跳到旧环境。
原因:生成二维码时把完整 URL 硬编码在代码里,域名一变,所有码指向错误。
解决:短期在旧域名上做 301 跳转到新域名,保住已贴的码;长期把域名抽成 3.1 里说的qr-base-url配置,并且改成“码里只放短码、跳转层拼域名”的结构,这样换域名只需要改跳转层,物理标签一张都不用动。这一条是我认为这份源码最值得保留的设计思路,比自己重写一套都值。
6. 验收:用脚本把“扫码—报修—派单—完工”跑一遍再交付
手工点一遍只能证明主流程通,证明不了边界。我习惯在交付前跑一个脚本,把整条链路按真实顺序走一遍,顺便把二维码的解码也验了。
import io, requests from PIL import Image from pyzbar.pyzbar import decode BASE = "http://127.0.0.1:8080" s = requests.Session() def scan(code: str) -> str: """模拟扫码:请求入口,从 302 的 Location 里取票据""" r = s.get(f"{BASE}/r/{code}", allow_redirects=False, timeout=5) assert r.status_code == 302, f"入口未跳转: {r.status_code}" loc = r.headers["Location"] return loc.split("ticket=")[-1] def submit(code: str, ticket: str) -> str: r = s.post(f"{BASE}/api/report/submit", json={ "deviceCode": code, "faultType": 2, "description": "验收脚本自动提交,请忽略", "reporterPhone": "13800000000", "ticket": ticket, }, timeout=5) r.raise_for_status() return r.json()["data"]["orderNo"] def grab(order_no: str, uid: int) -> bool: r = s.post(f"{BASE}/api/order/{order_no}/grab", json={"repairerId": uid}, timeout=5) return r.json()["data"] is True # 并发时这里应当只有一个人拿到 True # 1) 二维码内容自检:生成的文件解码结果必须和配置的前缀一致 text = decode(Image.open("./qr_out/A101.png"))[0].data.decode() assert text.startswith("https://repair.your-domain.com/r/"), text # 2) 走一遍完整链路 no = submit("A101", scan("A101")) print("提交成功:", no, "抢单:", grab(no, 1001))脚本里第一段做的是二维码自检,把生成的 PNG 重新解码,比对前缀。这一步看着多余,实际上能挡住“生成时用了测试域名、打印时没发现”这类事故。第二段把入口、提交、抢单串起来,抢单那一步如果并发跑,正常结果应当只有一个是True,多线程跑 20 次就能验证 4.2 的并发控制是否真的生效。
验收清单我一般固定成这几项,逐项打勾:
| 检查项 | 方法 | 通过标准 |
|---|---|---|
| 二维码可扫 | 手机扫码加脚本解码 | 两者都能直达报修页 |
| 免登录提交 | 清空 cookie 后扫码 | 能提交,且只能提交一次 |
| 状态流转 | 依次执行派单、接单、完工 | 状态与时间戳同步更新 |
| 并发抢单 | 20 线程同时接单 | 只有 1 个成功 |
| 超时提醒 | 手动把create_time改早 40 分钟 | 触发一次提醒,不重复 |
| 图片上传 | 传一张 4MB 手机原图 | 成功且方向正确 |
| 换域名演练 | 改qr-base-url后重新生成一张 | 新码可用,旧码有跳转 |
那几条里最容易被跳过的是最后一条,而它恰恰是唯一会造成物理返工的风险点。我之前吃过一次亏:验收时所有功能都过,上线两周后系统迁域名,贴出去的几百张码全部指向旧地址,最后是打印了新的贴纸挨个覆盖,人力成本远超开发成本。从那以后,无论多小的项目,换域名演练都必须走一遍,而且要在打印之前走,不是上线之后。希望帮到你。
本文还有配套的精品资源,点击获取