在日常测试工作中,测试工程师经常会遇到这样的烦恼:
有时候仅仅为了把一个截图发给开发,可能要经历「拍照 → 传到自己手机 → 微信转发 → 再传到 PC」这样的繁琐流程。
基于这些痛点,我开发了一个轻量级的测试效能小工具 —— 扫码上传助手 🧩。
它的目标非常简单: 让测试机与 PC 之间传文件,像发微信一样方便。
每次启动时,PC 端会自动生成一个上传二维码,并展示在浏览器页面上。 扫码后即可进入对应的上传通道。

测试机打开微信扫码(或浏览器扫码)二维码后,自动跳转到上传页面。
上传文件(截图、录屏、日志等)后,PC 端页面会实时接收并显示文件。  


除了文件,测试人员还可以直接发送文字消息到 PC 端,比如:
「复现步骤」「异常时间点」「接口返回错误码」等。

手机端无需登录账号,也不需要安装 App。
只要能扫码、能上传,就能使用。
真正实现测试机与 PC 的轻量级高效互通。
整个系统采用 前后端分离架构:
📱 手机端(H5 上传页面)
⇅ HTTP / WebSocket
🖥️ PC 端(浏览器显示 + WebSocket 监听)
⇅
☁️ 服务端(Spring Boot)
通过 ZXing 生成带任务 ID 的二维码,扫描后跳转上传页:
@Slf4j
@RestController
@RequestMapping("qrcode")
public class QrCodeController {
    @Value("${frontend.url}")
    private String frontendUrl;
    @GetMapping("/{taskId}")
    public ResponseEntity<String> generateQRCode(@PathVariable String taskId) throws Exception {
        String baseUrl = frontendUrl + "/upload?taskId=" + URLEncoder.encode(taskId, StandardCharsets.UTF_8.name());
        int width = 300, height = 300;
        Map<EncodeHintType, Object> hints = new HashMap<>();
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        BitMatrix bitMatrix = new QRCodeWriter().encode(baseUrl, BarcodeFormat.QR_CODE, width, height, hints);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        MatrixToImageWriter.writeToStream(bitMatrix, "PNG", baos);
        String base64 = "data:image/png;base64," + Base64.getEncoder().encodeToString(baos.toByteArray());
        return StatusCode.OK.build(base64);
    }
}
支持多文件与文字消息上传,上传后通过 WebSocket 实时推送到前端展示:
@PostMapping("/files")
public ResponseEntity<List> upload(@RequestParam String taskId,
                                   @RequestParam(value = "files", required = false) List<MultipartFile> files,
                                   @RequestParam(value = "content", required = false) String content) throws IOException {
    List<UploadMessage> list = new ArrayList<>();
    // 发送文本消息
    if (content != null && !content.isEmpty()) {
        UploadMessage textMsg = new UploadMessage();
        textMsg.setTime(LocalDateTime.now().toString());
        textMsg.setContent(content);
        UploadWebSocket.broadcast(taskId, textMsg);
        list.add(textMsg);
    }
    // 上传文件
    if (files != null) {
        for (MultipartFile file : files) {
            String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
            String uploadDir = staticLocation + "/upload/other/";
            File dest = new File(uploadDir, fileName);
            dest.getParentFile().mkdirs();
            file.transferTo(dest);
            UploadMessage msg = new UploadMessage();
            msg.setTime(LocalDateTime.now().toString());
            msg.setFileUrl("static/upload/other/" + fileName);
            UploadWebSocket.broadcast(taskId, msg);
            list.add(msg);
        }
    }
    return StatusCode.OK.build(list);
}
每个二维码对应一个 WebSocket 任务通道,用于推送实时消息:
@Slf4j
@Component
@ServerEndpoint(value="/ws/upload/{taskId}")
public class UploadWebSocket {
    // taskId -> set of sessions (多个客户端可以订阅同一个 taskId)
    private static final Map<String, Set<Session>> SESSION_MAP = new ConcurrentHashMap<>();
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private String taskId;
    @OnOpen
    public void onOpen(Session session, @PathParam("taskId") String taskId) {
        this.taskId = taskId;
        SESSION_MAP.computeIfAbsent(taskId, k -> Collections.newSetFromMap(new ConcurrentHashMap<>())).add(session);
    }
    @OnMessage
    public void onMessage(Session session, String message, @PathParam("taskId") String taskId) {
        // 可处理客户端消息(例如心跳),这里忽略
    }
    @OnClose
    public void onClose(Session session, @PathParam("taskId") String taskId) {
        Set<Session> set = SESSION_MAP.get(taskId);
        if (set != null) set.remove(session);
    }
    @OnError
    public void onError(Session session, Throwable thr) {
        // 日志忽略
    }
    public static void broadcast(String taskId, UploadMessage msg) {
        Set<Session> sessions = SESSION_MAP.getOrDefault(taskId, Collections.emptySet());
        if (sessions.isEmpty()) return;
        try {
            String text = MAPPER.writeValueAsString(msg);
            // 迭代发送(并发安全)
            for (Session s : sessions) {
                if (s.isOpen()) {
                    try { s.getBasicRemote().sendText(text); } catch (Exception ignored) {}
                }
            }
        } catch (Exception ignored) {}
    }
}
1️⃣ 启动服务端
运行 Spring Boot 项目,打开浏览器访问二维码生成页。
2️⃣ 扫描二维码
测试机用微信 / 浏览器扫码,跳转上传页面。
3️⃣ 上传文件或消息
上传截图、视频或输入文字说明。
4️⃣ PC 端实时显示结果
上传后内容自动推送并显示在 PC 端界面。
这个 扫码上传工具 虽然小巧,但解决了我们测试工作中一个长期的痛点。它让测试人员能更专注于 问题定位与分析,而不是被低效的文件传输方式拖慢节奏。
如果你也经常为测试机与 PC 之间传文件发愁,不妨试试开发这个工具 👇💬 欢迎留言告诉我你的建议!后续我还计划支持:上传后自动通知开发(Webhook)。