ARTICLE DETAIL

资讯详情

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

OpenHarmony上Flutter二维码扫描与短信生成实战

OpenHarmony上Flutter二维码扫描与短信生成实战 1. 项目概述与技术选型思考1.1 这个项目到底在做什么最近在做一款基于 Flutter for OpenHarmony 的二维码扫描 App核心功能就两条一是调用摄像头实时扫码二是能把短信内容生成二维码让别人扫。其中短信二维码这块不是简单把一行文字变成图片而是要按照短信协议去构造内容再落成二维码图案最终要能被主流手机相机直接识别、点开就跳转短信发送页面。这个链路听起来不复杂但落到 OpenHarmony 平台上的时候从工具链到渲染引擎处处都有以前在 Android 上没踩过的坑。这个项目适合谁看正在做 OpenHarmony 应用开发、想用 Flutter 跨端复用的团队会最有共鸣想了解二维码生成和解码底层原理、或者想学 Flutter 状态管理尤其是 Provider 用法的开发者也能从中捞到不少可直接抄的代码。我把整个项目的拆解、选型、编码过程和踩坑记录都整理在这篇里了。项目本身的技术栈是 Flutter OpenHarmony SDK扫码部分用了摄像头预览流加二维码解码库短信二维码生成部分用 Dart 侧二维码编码库直接产出矩阵数据再通过 Canvas 绘制成图片。UI 层用到了 Provider 做跨页面状态共享毕竟扫码需要把结果带回上一个页面历史记录列表还得瞬间刷新总不能全靠路由传参硬怼。1.2 为什么选 Flutter 而不是原生或 uni-app在 OpenHarmony 上开发第一反应肯定是官方主推的 ArkTS 加 ArkUI。但如果你手里已经有一套成熟的 Flutter 业务代码或者团队主力是 Dart 开发者那从零迁移到 ArkTS 的成本反而更高。我的选择逻辑是这样的Flutter 的渲染引擎是自绘的UI 一致性跟平台关系不大OpenHarmony 适配分支上跑起来界面效果和 Android/iOS 几乎没差而 ArkTS/ArkUI 虽然原生但生态和第三方库还在爬坡期很多组件都要自己造轮子。那有人会问RN 和 uni-app 不是也能跨端吗单看包体积和渲染性能Flutter 在移动端依然是第一梯队而且 Dart 的单线程事件循环模型在做相机帧流处理、二维码解码这种高频任务时配合 Isolate 能绕开 JS 引擎的阻塞问题。OpenHarmony 的 Flutter 分支目前已经能跑主流插件camera 这类涉及底层能力的也有对应适配层可以接。所以最终我选了 Flutter并且把项目直接跑在了 OpenHarmony 真机上下面的内容全部基于真机实践。2. 环境准备与 OpenHarmony 适配要点2.1 工具链与 SDK 版本搭配这次项目用的 Flutter 仓库是 OpenHarmony 官方维护的 fork 分支不是谷歌原版 Flutter SDK。这个分支最大的不同是构建目标从 AAR/Android 工程变成了 HAP/OpenHarmony 工程所以环境变量、构建命令和依赖仓库都要跟着调。我的版本组合如下Flutter SDKOpenHarmony 分支基于 Flutter 3.x 代码基线OpenHarmony SDKAPI 9 及以上开发工具DevEco Studio 用于构建 HAP 和做签名包管理ohpm 用来安装 OpenHarmony 原生依赖pub 用来装 Dart 包这里有个比较坑的地方Flutter 分支要求你在flutter doctor里能识别到 OpenHarmony SDK 路径否则创建项目时不会生成原生壳工程。我第一次搭建时就是 Flutter 环境配好了OpenHarmony SDK 装好了但两边互不认账最后检查发现是需要手动在环境变量里加OHOS_SDK_HOME指向 SDK 根目录同时在项目里维护一份local.properties指定hwsdk.dir。另外必须提一下 OpenHarmony 的兼容性认证。设备要跑带 Flutter 引擎的 HAP涉及到底层运行时权限和系统 API 调用建议在开发早期就对照官方兼容性测试要求把需要申请权限的模块提前列出来。相机、存储这些高风险权限一旦在认证环节出问题后面返工成本很高。我们项目在扫码功能上主动申请了相机权限短信生成只涉及图片保存存储权限也要提前配好。2.2 OpenHarmony 上 Flutter 应用的基本形态在 OpenHarmony 上Flutter 应用不是直接构建出一个 .so 就完事而是以“原生壳 Flutter 引擎”的方式存在。项目里会有一个 OpenHarmony 工程作为入口 Ability它负责生命周期管理Flutter 引擎挂载到它的窗口上完成渲染。这跟 Android 里 FlutterActivity FlutterFragment 的思路一脉相承。实际打包产物里Flutter 引擎相关代码会编译成 .so业务代码仍然通过 AOT 或 JIT 方式打包到 HAP 里。如果你的 App 需要被其他 OpenHarmony 原生模块复用那可以把 Flutter 模块打成 HAR 包引入这就对应了搜索热词里大家常问的 Flutter AAR 概念——在 OpenHarmony 侧把它理解为 Flutter HARHarmony Archive集成方式即可。首次跑通这个壳工程的流程大致是先用 Flutter 分支创建项目再用 DevEco 打开生成的 OpenHarmony 工程目录配置签名后直接 Run。这个阶段最容易遇到编译失败因为 OpenHarmony 原生侧和 Flutter 侧是两个独立的构建体系你需要确保 DevEco 打包时把 Flutter 引擎的 .so 文件正确带进 HAP。实测最稳妥的做法是先跑一遍 Flutter 构建脚本生成引擎产物再回 DevEco 里点构建。2.3 二维码能力在 OpenHarmony 侧的落地方案二维码库的选型我建议直接在 Dart 层做而不是依赖原生 SDK 的能力。原因很简单二维码编码和解码的算法是标准的纯 Dart 实现的可移植性极强OpenHarmony、Android、iOS 三端代码完全共享不用为每个平台各写一套 platform channel。实际在项目里我用的是 barcode 库也有类似的 qr 库它内部实现了 QR 码编码器能够在 Dart 侧生成包含格式信息、版本信息、纠错码字的矩阵数据然后我用 CustomPainter 把矩阵画出来。而摄像头扫码的解码部分走的还是相机原生采集再把帧数据传给 Dart 侧的解码器。这里有个性能决策要说明如果把每一帧原始图像都往 Dart 丢内存和耗时都顶不住。我的方案是先把相机帧按需降采样成灰度图只把亮度数据传给解码器这样既省内存又能显著降低解码延迟。更详细的解码链路我会放到第 4 章里讲。3. 短信二维码生成实现3.1 短信二维码的内容格式sms: 与 SMSTO: 协议二维码本身只是一张图真正有价值的是编码进去的内容。短信二维码要做的就是让手机相机扫到之后直接唤起短信编辑页并且自动填好收件人和短信内容。这背后用的是 URI 协议两种常见写法我都实测过了协议格式示例效果sms:电话号码sms:13800138000唤起短信页收件人已填内容为空sms:电话号码?body内容sms:13800138000?body你好唤起短信页收件人和内容都填好SMSTO:电话号码:内容SMSTO:13800138000:你好部分老版本手机兼容此格式同样填好内容和收件人实测中sms:格式在新版系统的兼容性最好而且参数body要记得做 URL 编码。比如短信内容带中文、空格、 符号不编码的话扫码后内容会被截断。我写了一个小程序试过用Uri.encodeComponent对内容编码之后再拼进 URI扫出来的短信内容就完整了。这个细节别轻视很多人生成的二维码能被识别但跳转后内容不完整基本都是栽在这里。另外如果你的场景需要预填多个收件人可以用分号分隔号码例如sms:10086;10010?body查话费。但要注意的是分号在 URL 里不算安全字符最好也统一编码一次免得某些扫码 App 解析不同。3.2 二维码生成的核心原理QR 码的结构与纠错二维码生成不是简单把字符串丢给库然后出图底层有一套完整的编码逻辑。理解了这套逻辑你才知道为什么有时候二维码看起来密密麻麻扫不出来为什么同样内容换不同纠错级别清晰度完全不同。QR 码的基础结构包括版本Version1 到 40版本越高模块数量越多能容纳的数据量越大。版本 1 是 21×21 模块每升一级长宽加 4到版本 40 是 177×177。纠错级别Error CorrectionL、M、Q、H 四级L 级约可恢复 7% 的码字M 级约 15%Q 级约 25%H 级约 30%。纠错级别越高图形抗污损能力越强但能存的内容越少。编码模式数字模式、字母数字模式、字节模式。纯数字和纯字母内容的压缩率更高中文和特殊符号走字节模式需要按 UTF-8 编码。掩码Mask对矩阵做 XOR 处理目的是让二维码图案的明暗分布尽量均匀方便扫描识别。编码器会尝试 8 种掩码选惩罚得分最低的一种。短信二维码这种场景内容本身不长一般是版本 1 到 3 就够用了。但我在做 App 时暴露了纠错级别选项给用户默认值是 M 级这样既能保证二维码图案不会过于密集也能容忍一定程度的磨损、折痕或者摄像头离得远模糊的情况。如果你的二维码会印在易磨损坏的介质上比如贴在设备外壳贴纸上那应该选 Q 甚至 H 级稳定性优先。3.3 实操代码在 Flutter 中用 barcode 库生成短信二维码我直接贴一段可以跑通的核心代码。这里用的是barcode库它提供了Barcode.qrCode()工厂方法编码后得到BarcodeMatrix然后我通过CustomPainter把它渲染成任意尺寸的图片。import package:barcode/barcode.dart; import package:flutter/material.dart; import dart:ui as ui; Uint8List generateSmsQrCode({ required String phoneNumber, required String message, double size 400, int errorCorrectionLevel 1, }) { final content sms:${phoneNumber.trim()}?body${Uri.encodeComponent(message)}; // 创建QR码生成器设置默认纠错级别M final qrCode Barcode.qrCode( errorCorrectLevel: errorCorrectionLevel, // 0L, 1M, 2Q, 3H ); // 编码成矩阵 final matrix qrCode.encode(content); // 预留白边 quiet zone const padding 4; final width matrix.width padding * 2; final height width; // 用Canvas绘制黑白格 final recorder ui.PictureRecorder(); final canvas Canvas(recorder); final paint Paint()..color Colors.black; final cellWidth size / width; final leftOffset padding * cellWidth; for (var x 0; x matrix.width; x) { for (var y 0; y matrix.height; y) { if (matrix.get(x, y)) { // 矩阵里为true的格子涂黑 canvas.drawRect( Rect.fromLTWH( leftOffset x * cellWidth, leftOffset y * cellWidth, cellWidth, cellWidth, ), paint, ); } } } final picture recorder.endRecording(); final image picture.toImage(size.toInt(), size.toInt()); // 后续可以转成ByteData再存为PNG或直接以Image组件展示 return imageToPng(image); }这段代码最核心的动作是qrCode.encode(content)它会把短信 URI 转成一串布尔矩阵true 就是黑色模块。后面的绘制过程本质上是放大矩阵没有任何魔法。如果你用的是qr_flutter这类现成 Widget 库会更省事但换来的代价是自定义能力弱比如想在二维码中心加 Logo、想控制模块形状、想调整颜色渐变它们都很难做。我自己在项目里最终选择了自绘方案因为要给二维码加品牌角标和圆角模块样式这些业务需求决定了不能只靠现成 Widget。3.4 生成短信二维码的细节优化先说说错误处理。短信内容为空时生成出来的二维码内容是sms:号码?body扫出来虽然也能唤起短信页但用户会看到一个空短信体验很怪。我当时在生成前加了一道校验内容和号码必须至少有一项非空否则就提示用户。这个逻辑看着简单却是产品上线后最容易被打回来的交互细节。然后是二维码尺寸和留白。二维码周围一定要留白专业叫 quiet zone标准要求是至少 4 个模块宽度。代码里我写了padding 4翻译成像素后就是二维码总图的一部分。千万别为了贴边排版把白边裁掉很多扫码失败都是二维码贴到图片边缘、识别器找不到定位图案引起的。再提一下图片坐标系和 Canvas 的关系。绘制二维码时尺寸参数size只是逻辑像素如果你要保存高清图建议直接用ui.PictureRecorder渲染到更大幅面的 Canvas 上而不是在 Widget 里用截屏方式生成图片。截屏生成图片会受到屏幕分辨率和 DPR 影响清晰度不稳定。我在代码里直接指定size 400或更高比如 800、1024这样得到的 PNG 用于打印物料也足够清晰。最后加 Logo 的坑。给二维码中心加图标可以增强品牌感但会遮挡部分数据区域Abort 风险全靠纠错级别兜着。如果加了 Logo 还选 L 级纠错大概率现场扫码翻车。我给用户提供的默认配置是有 Logo 时强制 Q 级以上纠错同时 Logo 尺寸不要超过二维码总宽度的 1/4中心遮挡位置尽量控制在固定图案之外的区域。4. 扫码能力与 Camera 集成4.1 相机配置与权限申请OpenHarmony 上的摄像头调用如果直接写原生代码需要走 Camera Kit 服务涉及权限声明、设备回调、会话配置好几层。但在 Flutter 侧我用了 camera 插件的 OpenHarmony 适配分支这样 Dart 代码基本不用改底层能力由插件映射到 OpenHarmony 的相机接口。使用之前必须在module.json5里申请ohos.permission.CAMERA而且 OpenHarmony 对相机权限的管理比 Android 更严格运行时还得再动态申请一次。final cameras await availableCameras(); final controller CameraController( cameras.first, ResolutionPreset.medium, enableAudio: false, ); await controller.initialize();摄像头方向是扫码 App 一个隐藏 Bug 重灾区。OpenHarmony 设备上相机传感器的方向和屏幕方向不一定一致预览画面经常是旋转 90 度或 180 度的如果不做校正用户看到的画面是躺着的解码结果坐标对不上扫码框扫码体验直接归零。我用的是 controller 提供的value.deviceOrientation加value.sensorOrientation做预览旋转同时把预览画面用RotationTransition按需旋转确保画面始终竖直。对焦策略也要单独说。扫码场景对自动对焦的依赖很高尤其是近距离扫码时camera 插件默认的对焦模式经常不够果断。我的做法是启用连续自动对焦await controller.setFocusMode(FocusMode.auto); await controller.setExposureMode(ExposureMode.auto);如果设备支持点按对焦还可以在扫码框里让用户点击画面触发setFocusPoint。这个细节对工业场景特别有用——贴纸上二维码很小自动对焦容易拉到背景上点按对焦能救回不少识别率。4.2 解码流程与性能优化从摄像头到识别出二维码整条链路是这样的CameraController 启动预览通过startImageStream或者定时抓帧拿CameraImage。把CameraImage转成解码器需要的灰度图像数据。这一步做了降采样比如原始分辨率为 1280×720我通常缩到 320×240 左右再解码识别速度提升好几倍。灰度图转成二维码解码器的亮度源LuminanceSource。解码器返回结果字符串解析出是否是sms:协议是的话跳转短信编辑页。Dart 侧的二维码解码我用的是zxing2库它封装了 ZXing 核心解码逻辑。这里重点说性能问题startImageStream的回调频率非常高每秒 30 帧左右如果每一帧都做完整解码CPU 会瞬间拉满手机发烫掉帧。我的优化策略是加入一个简单的节流器只有距离上一帧解码结束超过 200ms 才处理下一帧同时维护一个失败次数计数器连续 5 帧解码失败后自动降低抓帧频率到每秒 5 帧直到画面变化幅度较大再恢复正常频率。还有一个容易被忽略的点解码运算如果放在 UI isolate扫码页面会出现明显的掉帧卡顿。我把解码逻辑放进了独立 Isolate通过ReceivePort通信把解码结果传回主 isolate。这样做的好处是相机预览线程和解码线程互不阻塞用户扫码时可以明显感觉到画面流畅度上升尤其是低端设备上效果非常显著。final decodeIsolate await Isolate.spawn(decodeLoop, receivePort.sendPort); void decodeLoop(SendPort sendPort) { final port ReceivePort(); sendPort.send(port.sendPort); port.listen((message) { // 接收灰度图执行解码回传结果 }); }4.3 扫码框与用户体验细节扫码框 UI 我不建议直接用截图渲染一个半透明黑色遮罩和绿线那只是表面功夫。真正影响体验的是从“相机图像坐标”到“屏幕显示坐标”的换算关系。解码器返回的二维码位置是基于裁剪后的灰度图的需要换算回预览画面的坐标系再换算到屏幕坐标系最后判断是否落在扫码框内部。我踩过一个真实案例扫码框画在屏幕中间偏上二维码也显示在框内但解码结果却经常扫到背景里的另一个二维码。原因就是坐标换算少做了一步——我直接用整帧灰度图解码没有按扫码框区域裁剪后再解码。后来改成先根据扫码框在预览流中的位置裁出子图再去解码误扫率一下降到接近零。5. 状态管理与组件通信5.1 为什么用 Provider 管理扫码结果扫码页面和历史记录页面之间需要共享数据扫到一条短信二维码内容我要立刻跳转回首页同时在历史记录里追加一条新记录用户从历史记录里点击某条记录要能反向解析出来并再次生成二维码。这个场景如果用setState各管各的代码很快就会乱成一锅粥。我在项目里用 Provider 做全局扫描历史和配置项的托管理由是它轻量、官方维护、心智负担小对 OpenHarmony 上的性能占用也可控。对比之下Bloc 和 Riverpod 不是不好而是配重不同。一个扫码工具类的 App核心状态就是“历史记录列表”、“当前配置项”、“最后一次扫码结果”用 ChangeNotifier 足够覆盖不必要引入繁琐的代码生成。Provider 的好处在于它对 Dart 开发者来说几乎没有学习曲线而且调试时能看到notifyListeners被谁触发的调用栈比 EventBus 的满天飞事件好查得多。5.2 Provider 核心用法实战我在项目里的状态类是这么写的class ScanHistoryModel extends ChangeNotifier { ListScanRecord _records []; ScanRecord? _lastResult; ListScanRecord get records List.unmodifiable(_records); void addRecord(ScanRecord record) { _records.insert(0, record); _lastResult record; notifyListeners(); } }页面里通过ConsumerScanHistoryModel监听变化历史列表实时刷新。这里有个细节列表数据在notifyListeners后如果重建得太过频繁会导致滑动中的列表跳变。我的建议是历史列表只用records.length变化作为监听触发条件而扫码结果浮层则单独用lastResult的变化驱动两者分开监听避免一次扫码把整个列表和浮层都重建一遍。跨组件通信方面我用了三种手段同页面局部状态直接setState比如扫码框动画。跨页面共享数据Provider比如历史记录。一次性事件传递路由参数比如用户点开历史记录详情。我当时看到很多帖子在问 Flutter 组件通信到底用什么其实没有银弹。同一个页面内兄弟组件要通信最简单的是把回调函数传下去跨模块共享数据用 Provider 这类 InheritedWidget 封装全局一次性消息用 EventBus 也不是不行但你要能承受“事件无法确定最终有没有被消费”的心智负担。我这边的原则是能用参数传递就不上全局状态能用局部状态就不用 Provider乱用全局状态只会让 bug 更难追踪。5.3 组件通信与回调的边界Provider 解决的是“共享”但很多场景其实只需要“通知”。比如扫码框页面里相机启动完成后要通知顶栏按钮启用闪光灯开关这个用回调就很自然CameraPreviewWidget( onCameraReady: () { setState(() _flashEnabled true); }, )很多新手容易一上来就迁出三四个 Provider结果页面刷新时中间的无关 Widget 也跟着 rebuild。我在项目里给团队定了个规矩真正需要持久化的数据或者跨 Tab 共享的数据才进 Provider页面内的瞬时状态一律留在页面级 State 里。这也是为什么我的扫码页看起来用了 Provider但结构性状态并没有全部堆进去。6. 常见问题与排查技巧实录6.1 Flutter 运行崩溃dart_vm_initializer 报错搜索热词里出现了flutter/runtime/dart_vm_initializer.cc(41)这种报错我也真遇到过。这类错误本质上是 Flutter 引擎在初始化 Dart VM 时失败常见原因有三类Dart 侧缺失依赖库文件、原生壳工程没有正确加载 Flutter 引擎资源、或者 AOT 产物和当前平台架构不匹配。在 OpenHarmony 上最常见的是 HAP 打包时没有把 arm64-v8a 对应的 Flutter .so 带进去。排查思路是先分两层看如果一打开 App 就瞬间崩溃优先检查引擎和 .so 是否正常如果崩溃发生在使用特定功能时优先检查是不是动态库在运行时加载失败。可以先用flutter build hap --debug构建一版再在 DevEco Studio 里看 crash 日志的 native stack。大多数时候恢复方式是清理本地的build目录、重新生成壳工程文件、重新执行flutter pub get三步齐下就能解决。6.2 Impeller 渲染引擎在 OpenHarmony 上的启用情况Impeller 是 Flutter 新一代渲染引擎替代旧 Skia 后能解决帧率卡顿问题。在 Android 上有不少机器已经默认启用但在 OpenHarmony 的 Flutter 分支上Impeller 的适配并不彻底。我实测发现OpenHarmony 上开启 Impeller 后部分设备会出现文字模糊或异常闪烁的现象尤其是在频繁绘制大图比如全屏二维码预览的时候更容易触发。如果你的项目在 OpenHarmony 上遇到渲染异常最直接的排查手段是用命令行参数禁用 Impellerflutter run --no-enable-impeller如果这种方式能解决说明问题就出在 Impeller 的 OpenHarmony 后端不完善而不是业务代码本身。目前我的选择是 OpenHarmony 发布包默认走旧的 Skia 渲染路径等官方分支把 Impeller 后端的 GPU 队列和纹理回收逻辑稳定了再切过去。6.3 下拉刷新失效与列表滑动冲突历史记录页我加了下拉刷新RefreshIndicator结果运行起来经常出现“列表到底了往下拽不出刷新指示器”的问题。原因很清楚列表内容太少时ListView 没有填满屏幕默认physics不允许滚动RefreshIndicator 收不到触发手势。解决办法是在 ListView 上强制开启滚动ListView( physics: const AlwaysScrollableScrollPhysics(), )另外如果列表嵌套了横向滑动组件或者可缩放组件手势竞技场会优先匹配子组件的滑动导致下拉刷新响应变慢。我后来在扫码历史记录页做了一个横向滑动的时间筛选栏这个栏目里滑动的 GestureDetector 会和 RefreshIndicator 抢手势最终我通过给筛选栏设置behavior: HitTestBehavior.opaque加上对纵向滚动手势不做处理才解决冲突。这类问题不如编译报错显眼但实际开发中出现频率极高建议大家在设计 UI 时就把手势冲突考虑进去。6.4 Flutter AAR 集成到原生工程时的 Gradle 插件问题热词里有人问you are applying flutters main gradle plugin imperatively using the apply这个报错这是把 Flutter module 以 AAR 方式集成到既有原生工程时因为新建工程默认用的是新版本 Gradle 插件解析方式而你的原生工程还在用老式apply写法导致的。在 OpenHarmony 语境里类似的问题会发生在“既有 OpenHarmony 工程接入 Flutter 模块”的场景。我的建议是直接统一使用 plugins DSL 方式不要混用apply。具体可以看原生工程的settings.gradle里是否正确声明了 Flutter SDK 路径和 module 依赖OpenHarmony 构建链路里还要保证 ohpm 的仓库配置和 Flutter 产物目录在同一个项目结构下。这类问题排查起来头绪多但从业者最重要的经验是不要在一个工程里维护两套插件加载逻辑能统一就统一。6.5 新建项目跑不起来的典型原因很多人在 OpenHarmony 上刚创建完 Flutter 项目直接跑就报错。我遇到的典型场景是创建项目后没有执行flutter pub get导致.dart_tool缺失编译器找不到包或者是 DevEco 的 SDK 版本和 flutter 分支要求的 API 不匹配再或者就是签名配置没做设备上安装时报 INSTALL_PARSE_FAILED。我的建议是创建项目后第一件事跑flutter pub get然后再用 DevEco 打开工程真机连接后先跑一个最小 Demo 验证设备链路正常再逐步加入 camera、Provider 这些依赖。不要一上来就堆功能否则出了问题你会分不清是设备问题、依赖问题还是业务代码问题。7. 结尾一点个人感受这个项目从环境搭建到扫码和短信二维码生成跑通前后折腾了我差不多两个周末。最大的体会是Flutter 在 OpenHarmony 上做工具类应用已经完全可行但你必须接受“有些插件需要手动找适配分支、有些本地能力要用 platform channel 自己搭桥”的现状。二维码生成和扫码解码这种标准算法能力尽量留在 Dart 层实现可移植性强也少操原生适配的心而摄像头、存储这类强平台能力则该用插件就用插件不要自己从零封装。最后再分享一个小技巧你在做短信二维码的时候务必要分别拿微信、系统相机和各类扫码 App 都扫一遍不同 App 对 URI 的解析宽容度差异很大提前测一轮能省掉不少售后问题。
返回列表