简介:面向 C#/WPF 开发者的二维码生成与识别示例项目,以 ZXing.Net(zxing.dll)为核心,完成 QR Code 的编码与解码,并结合 EPFMediaKit.dll 支持摄像头/图片场景下的识别,适合需要在桌面应用中快速集成二维码功能的中级开发者。压缩包为 RAR 格式,共 148 个文件,大小 22.48MB,其中包含 35 个 DLL(核心依赖库)、31 个 PDB(调试符号)、29 个 XML(配置或文档说明)、12 个 CS(C# 源码)、2 个 XAML(界面布局),以及 EXE、SLN、CONFIG 等工程文件,目录结构清晰,与 Visual Studio 解决方案保持一致。已有 1002 人学习/浏览,关注度较稳定。示例并非只展示单一接口调用,而是完整覆盖从二维码内容输入、BarcodeWriter 生成位图、WPF Image 控件绑定展示,到摄像头图像采集、解码识别、结果显示的闭环流程;同时附带编译缓存、资源文件和项目配置文件,打开即可还原出可运行的 Visual Studio 工程,便于直接运行对照学习或在其基础上二次开发。通过关键 API 调用,还能看到异常处理、线程与 UI 交互、性能优化等实际落地思路,帮助中初级开发者少走弯路,快速掌握 ZXing.Net 在 WPF 项目中的集成方法。
1. C# WPF二维码生成与识别:一套能从零跑到交付的ZXing.Net落地流程
做C# WPF桌面工具时,二维码的生成和识别几乎是躲不开的组件。前一阵给某个信息管理Demo补上标签打印功能,需要批量生成二维码、再通过摄像头扫回,原本以为用ZXing.Net写个生成和识别demo是半小时的事,真正动手才发现:中文内容编码、图像格式、UI线程、摄像头抓帧节奏,每一环都能把简单需求拖进坑里。这篇笔记围绕C# WPF环境下的二维码生成与识别Demo展开,覆盖ZXing.Net的选型理由、参数语义、识别策略和五条高频踩坑记录,适合正在用WPF做桌面工具、想让二维码功能一次跑通的开发者。
2. 选型与前置准备:为什么是ZXing.Net,以及环境怎么搭
先说结论:如果只需要生成二维码,QRCoder很轻快;但如果你身处WPF环境,而且早晚要接摄像头识别,选ZXing.Net是更实际的方案。理由有三点:一是ZXing.Net同时覆盖生成和识别两条链路,QRCoder只解决生成;二是ZXing.Net对多种条码格式有统一抽象,未来业务从QR码扩展到Code 128也不至于重写;三是它的渲染器绑定机制让WPF项目可以直接产出BitmapSource,省去一轮图像格式转换。对比下来,代价只是API稍显隐晦、参数多,但把参数吃透后就是纯收益。
2.1 C#生态里二维码库的取舍
在C#生态里,常见选择是QRCoder、ZXing.Net,以及一个已经很长时间没维护的老库。老库API停留在早期.NET Framework风格,直接排除。QRCoder的生成API非常友好,单文件就能出图,但它在识别领域没有对应组件,想识别还得另引入一个库,两套库的图像类型、错误处理方式完全不同,调试起来很割裂。ZXing.Net则保持了和Java版ZXing一致的能力边界:解码器支持QR_CODE、EAN_13、CODE_128等多种格式,还提供BarcodeReader和BarcodeWriter两个核心入口。对桌面端来说,这个边界意味着“生成和识别”是一个整体能力,而不是两个零散功能。
| 对比项 | QRCoder | ZXing.Net |
|---|---|---|
| 功能方向 | 仅生成 | 生成 + 识别 |
| 码制支持 | QR码为主 | QR、Code128、EAN等 |
| WPF输出 | 需要手转BitmapSource | 有Binding包直接输出 |
| 摄像头识别 | 无 | BarcodeReader逐帧解码 |
| 维护状态 | 稳定但边界有限 | 活跃,社区问题沉淀多 |
这个对比表不是想说QRCoder不行。我见过不少项目仅用QRCoder也跑得很好,但如果需求清单里已经有“扫码枪/摄像头识别”这一条,从第一天就选ZXing.Net,后面省掉的不只是适配工作,还有两套图像类型之间的转换代码和心智负担。
2.2 NuGet包安装与版本锁定
创建WPF项目后,在NuGet里装两个包。第一个是ZXing.Net主包,第二个是ZXing.Net.Bindings.Windows.Presentation,它提供WPF专用的BitmapSource渲染器。安装命令如下:
Install-Package ZXing.Net -Version 0.16.9 Install-Package ZXing.Net.Bindings.Windows.Presentation -Version 0.16.7这个版本组合是反复踩过坑之后固定的。如果只装主包不装Binding包,BarcodeWriter<BitmapSource>这个泛型就没法用,因为缺少渲染器。如果主包和Binding包版本差太远,运行时会抛“找不到类型BitmapSourceRenderer”的错误,这类错误编译期不报,运行到一半才炸,排查起来很费时间。
提示:如果项目目标框架是.NET Framework 4.7.2而不是.NET 6,包版本要往下选,比如ZXing.Net 0.16.4配同版本的Binding包,否则容易遇到程序集加载异常。
2.3 Demo的最小项目结构
这个Demo按“界面、服务、入口”三个层次拆文件。MainWindow.xaml只放布局,MainWindow.xaml.cs里写按钮事件响应,QrCodeService.cs负责所有二维码生成与识别的具体逻辑。不引入MVVM框架,因为Demo规模不大,加框架反而增加理解成本。
| 文件 | 职责 |
|---|---|
| MainWindow.xaml | 输入框、按钮、二维码Image展示区 |
| MainWindow.xaml.cs | 按钮事件、调用Service、结果回显 |
| QrCodeService.cs | 封装BarcodeWriter和BarcodeReader调用 |
MainWindow.xaml的布局保持简单。界面包含一个TextBox输入内容、一个Image控件显示二维码、两个按钮分别触发生成和识别:
<StackPanel Margin="20" Width="420"> <TextBox x:Name="ContentBox" Height="30" VerticalContentAlignment="Center" Text="https://example.com/demo?id=001" /> <Button x:Name="GenerateBtn" Content="生成二维码" Click="GenerateBtn_Click" Height="32" Margin="0,12,0,0" /> <Image x:Name="QrImage" Width="300" Height="300" Stretch="Uniform" Margin="0,16,0,0" /> <Button x:Name="DecodeBtn" Content="从本地图片识别" Click="DecodeBtn_Click" Height="32" Margin="0,16,0,0" /> </StackPanel>TextBox初始给了一个带参数的URL示例,方便直接点生成看效果。Image的Stretch设为Uniform可以保证二维码在控件内等比缩放不变形。两个按钮各自走QrCodeService中的Generate和Decode方法。这个结构的好处是,后续接摄像头识别时只需要在QrCodeService里新增一个DecodeFromFrame方法,界面层不用动。做这类工具型Demo时,把服务边界划清楚比用不用MVVM重要得多。
3. 生成二维码:编码参数决定你有没有坑
生成二维码虽然核心代码只有几行,但参数背后的含义值得抠细。很多“生成出来扫码扫不了”的问题,根子不在库,而在参数设置。这一章从最基本的生成代码讲起,逐个拆参数,再覆盖中文编码和保存图片两个高频场景。
3.1 第一版生成代码:BarcodeWriter 的用法
在WPF项目里,生成二维码的标准写法是构造一个BarcodeWriter<BitmapSource>,指定Format和Options,然后调用Write方法:
using ZXing; using ZXing.QrCode; using System.Windows.Media.Imaging; public BitmapSource GenerateQrCode(string content) { var writer = new BarcodeWriter<BitmapSource> { Format = BarcodeFormat.QR_CODE, Options = new QrCodeEncodingOptions { Width = 300, Height = 300, Margin = 1, CharacterSet = "UTF-8", ErrorCorrection = ZXing.QrCode.Internal.ErrorCorrectionLevel.M } }; return writer.Write(content); }代码逻辑本身不复杂,但几个参数需要弄清。Format决定了编码器用哪套规则,二维码必须传BarcodeFormat.QR_CODE。Width和Height是最终输出图片的像素尺寸,不是码的模块数,相同内容在不同尺寸下显示的模块密度不同。Margin是二维码四周静区的宽度,单位是模块,印刷场景下至少留1。CharacterSet指定内容编码,处理中文时是关键。ErrorCorrection是容错级别,M级在实际使用中性价比最高。
提示:
BarcodeWriter<T>的泛型能不能用BitmapSource,取决于是否安装了ZXing.Net.Bindings.Windows.Presentation。如果没安装Binding包,可用的泛型是BarcodeWriter<Bitmap>,但那是WinForms系图像类型,在WPF里还需要再转一次,凭空多一道坑。
3.2 尺寸、静区与容错级别的组合策略
二维码的打印和显示场景差别很大,直接套默认参数容易出问题。我在做标签功能时试过几组组合,整理成一张表:
| 场景 | 建议尺寸 | 容错级别 | Margin | 说明 |
|---|---|---|---|---|
| 屏幕显示 | 256~512 | M | 1 | 清晰度足够且生成快 |
| 热敏纸小票 | 512~768 | H | 2 | 热敏纸易污损,高容错 |
| 标签机打印 | 300~400 | Q | 1 | 面积有限,Q级平衡 |
| PDF存档 | 1024 | M | 2 | 放大后细节保留完整 |
这里有一个容易忽略的点:同一个二维码内容,容错级别越高,模块总数越多,图案越密。同样在300x300像素下,H级比L级多出不少模块,视觉上格子更密,如果打印精度不够,反而更容易糊在一起。所以高容错并不总是安全的,要结合介质的打印精度来选,而不是无脑上H级。
3.3 中文内容与URL转义
中文内容在二维码里几乎必踩一次坑。最常见的问题是生成中文二维码后,手机扫码出来是乱码。原因是编码选项里的CharacterSet没设置成UTF-8,或者调用方传入的字符串本身编码来源就不对。正确做法如下:
string content = "标签编号:ABC-2024-001"; var options = new QrCodeEncodingOptions { Width = 300, Height = 300, Margin = 1, CharacterSet = "UTF-8", ErrorCorrection = ZXing.QrCode.Internal.ErrorCorrectionLevel.M }; var writer = new BarcodeWriter<BitmapSource> { Format = BarcodeFormat.QR_CODE, Options = options }; BitmapSource qrImage = writer.Write(content);说明一下CharacterSet这个参数为什么不设也能生成:ZXing内部有默认字符集,默认值在部分平台上按ISO-8859-1处理,遇到中文就输出错误字节。显式写成UTF-8,等于告诉编码器“按UTF-8生成字节序列”,扫码枪端按UTF-8解析就能对上。
如果内容是网址且带中文参数,建议先转义再放进二维码:
string name = Uri.EscapeDataString("张三"); string url = $"https://example.com/query?name={name}&id=001";扫码枪扫描二维码拿到的是URL字符串,但中文参数如果没有预先转义,部分扫码枪在打开浏览器时会把中文按本地编码拼进URL,导致服务端解析失败。用Uri.EscapeDataString之后,二维码里存的就是百分号编码形式,兼容性好得多。
实际项目中还遇到过一种情况:同一套代码,在一台机器上生成的中文能正常扫,在另一台机器上生成就乱码。排查后发现是源文件编码不同,代码文件以GB2312保存时,字符串常量在编译期就出现了字节差异。解决方案是统一把代码文件保存为UTF-8格式,并在生成前用string.IsNormalized(NormalizationForm.FormC)检查内容规范性。
3.4 保存为PNG:目录与文件覆盖的坑
生成出的BitmapSource要落盘时,最稳妥的方式是用PngBitmapEncoder。这里面也有两个坑,一个是目录不存在,一个是文件重名的覆盖行为。我的习惯是先判断目录再决定是否创建:
private void SaveQrCode(BitmapSource qrImage, string filePath) { string directory = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(qrImage)); using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write); encoder.Save(stream); }先处理目录,保证FileStream打开时目标文件夹一定存在;然后把BitmapSource放进PngBitmapEncoder的Frame集合;最后通过FileStream写盘。FileMode.Create会覆盖已有文件,适合批量导出时直接覆盖旧图。如果需求是“不允许覆盖,避免误操作”,把FileMode.Create改成FileMode.CreateNew,文件已存在时会抛出IOException,调用方捕获后提示用户。
保存格式上,PNG是无损压缩,二维码边缘清晰,没有JPEG的模糊晕影,扫码成功率更高。不建议用JPEG保存二维码,除非打印场景对文件大小有严格限制。
4. 识别二维码:图像输入与解码参数的双重把控
识别比生成复杂,因为输入是图像,图像质量直接影响解码结果。ZXing.Net的解码入口BarcodeReader接收一个图像源,内部会做灰度化、二值化、找定位点、解码。用户能控制的参数不算多,但每个参数都对应一个真实的坑。
4.1 从本地图片识别的完整流程
读取本地图片并解码,最直接的方式是构造BitmapImage然后传给BarcodeReader:
using ZXing; using System.Collections.Generic; using System.Windows.Media.Imaging; public string DecodeFromFile(string filePath) { var bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.CacheOption = BitmapCacheOption.OnLoad; bitmap.UriSource = new Uri(filePath); bitmap.EndInit(); var reader = new BarcodeReader { Options = new DecodingOptions { TryHarder = true, PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.QR_CODE } } }; var result = reader.Decode(bitmap); return result?.Text ?? string.Empty; }参数说明里最关键的是CacheOption = BitmapCacheOption.OnLoad。如果不设置它,BitmapImage默认延迟加载,文件句柄可能被占用,后续删除或覆盖图片时会报“文件正在被使用”。TryHarder设置为true后,解码器对模糊、不平整、光照不均的图像会增加尝试次数。PossibleFormats限定为QR_CODE,避免在复杂场景下把其他条码误识别成二维码。
返回值处理上,result可能为null,所以用result?.Text ?? string.Empty来避免空引用异常。解码失败时返回空字符串,界面层可以根据是否为空做提示。还有一个性能细节:BarcodeReader每次解码都会新建LuminanceSource,频繁调用时内部会创建不少临时对象。性能敏感的场景建议复用BarcodeReader对象,而不是每次都new一个。
4.2 摄像头实时扫码:抓帧、解码分离
WPF里做摄像头扫码没有像WinForms那种直接拖一个控件就能看的方案,常见做法是用MediaCapture抓取视频流。MediaCapture初始化后,用一个定时器循环抓帧,把帧交给解码器。这个过程最大的坑是UI阻塞,解码比较耗时,必须放到后台线程:
private MediaCapture _capture; private readonly BarcodeReader _reader = new BarcodeReader(); private async void StartCapture() { _capture = new MediaCapture(); await _capture.InitializeAsync(); var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(150) }; timer.Tick += CaptureTimer_Tick; timer.Start(); } private async void CaptureTimer_Tick(object sender, EventArgs e) { using var stream = new InMemoryRandomAccessStream(); await _capture.CapturePhotoToStreamAsync(ImageEncodingProperties.CreateJpeg(), stream); stream.Seek(0); var bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.CacheOption = BitmapCacheOption.OnLoad; bitmap.StreamSource = stream.AsRandomAccessStream(); bitmap.EndInit(); bitmap.Freeze(); _ = Task.Run(() => { var result = _reader.Decode(bitmap); if (result != null) { Dispatcher.Invoke(() => resultBox.Text = result.Text); } }); }这里解释几个设计选择。为什么用CapturePhotoToStreamAsync而不是直接从VideoBrush里截帧:CapturePhotoToStreamAsync返回的是一帧JPEG图像,图像质量比截屏稳定,和BarcodeReader内部的图像解析兼容性更好。抓帧频率150毫秒,既不会让摄像头过度发热,也不容易漏掉短暂停留的二维码。Task.Run把Decode放到线程池线程,避免解码期间的UI卡顿。解码成功后用Dispatcher.Invoke回到UI线程更新文本框。
注意BitmapImage通过StreamSource加载后,调用了一次Freeze()。Freeze把BitmapImage标记为只读,之后可以安全地跨线程传递,这是WPF图像处理里的常见操作。忘记Freeze时,后台线程访问bitmap会抛出InvalidOperationException,排查起来比较绕。
4.3 识别率上不去的调节顺序
遇到识别失败时,我一般按下面的顺序排查,每步都看一眼结果再决定是否继续:
- 检查图像是否包含二维码主体,二维码是否占图像面积的30%以上;
- 检查图像是否过度压缩,JPEG质量低于60时模块边缘模糊严重;
- 先做灰度化和对比度增强,再送入解码器;
- 尝试PureBarcode模式,告诉解码器“画面里只有二维码”;
- 只有上面都无效时才改大图像尺寸或提高容错级别重新生成。
第四步的PureBarcode模式在纯二维码图像上识别速度更快,因为它跳过了部分边缘检测逻辑:
var reader = new BarcodeReader { Options = new DecodingOptions { TryHarder = true, PureBarcode = true, PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.QR_CODE } } };PureBarcode直译为“纯条码”,适合二维码单独占据整张图、没有背景干扰的情况。如果图像里除了二维码还有大片文字、表格线,或二维码旁边紧挨着其他条码,这个参数反而会降低识别率。所以它应该被当作一个优化项,而不是默认配置。在实时扫码场景里,画面中通常会有手指、桌面背景等干扰,我会先不开PureBarcode,等画面稳定后再动态切换。
5. 避坑与常见问题:五条翻车记录,希望你一条都不遇到
这个Demo在开发过程中踩了不少坑。每条记录按“现象→原因→解决”还原,方便遇到类似问题时直接对照。
5.1 中文二维码扫出来乱码
现象:用ZXing.Net生成的二维码,内容是一段中文标签名,手机扫码后显示“䏿–‡”“娴嬭瘯”之类的乱码。
原因:CharacterSet没有显式指定为UTF-8,ZXing内部默认按ISO-8859-1处理中文字符,导致字节序列不符合扫码端的解析预期。
解决:在QrCodeEncodingOptions里加上CharacterSet = "UTF-8",同时确保传入的字符串不是从GBK编码的文件里读出来的。如果字符串来源是GBK文件,先用Encoding.UTF8.GetString(Encoding.GetEncoding("GBK").GetBytes(raw))转码再生成。从那以后,只要是中文内容,我会在方法入口统一做一次字符集检查,把问题挡在生成之前。
5.2 尺寸越大识别率反而越低
现象:把生成的二维码图片从300x300放大到2000x2000,保存后发现手机扫码需要凑很近才能识别,有些角度直接失败。
原因:单纯放大像素尺寸并没有增加二维码本身的模块清晰度,反而让模块边缘出现插值模糊;过大的图像还可能导致解码器内部采样的模块比例偏离标准。
解决:放弃“越大越好”的想法,在300、512、768三档尺寸下分别打印测试,选择成功率最高的一档。放大显示时使用NearestNeighbor插值算法,而不是WPF默认的线性插值,这样能保持模块边缘锐利,减少插值带来的模糊干扰。
5.3 批量生成时UI卡成“未响应”
现象:点击“生成500张二维码”按钮后,窗口标题出现“未响应”,等了很久才恢复。
原因:BarcodeWriter的Write方法包含矩阵编码、掩码计算、图像渲染等多个阶段,在UI线程里循环执行会把消息循环堵死,系统判定窗口无响应。
解决:把批量生成放入Task.Run,每生成一张就通过Dispatcher推送一张预览图。推送频率上要控制:每生成一张就推送会频繁占用UI线程;每10张推送一次,界面看起来是分批刷新,整体流程顺畅很多。核心代码如下:
private async Task GenerateBatchAsync(IEnumerable<string> contents) { var results = new List<BitmapSource>(); await Task.Run(() => { int index = 0; foreach (string content in contents) { var qr = QrCodeHelper.Generate(content, 300, 1); results.Add(qr); index++; if (index % 10 == 0) { Dispatcher.Invoke(() => previewList.ItemsSource = results.ToList()); } } }); previewList.ItemsSource = results; statusText.Text = $"生成完成,共{results.Count}张"; }异步方法内部先开一个后台任务批量生成;每满10张调用一次Dispatcher刷新界面;全部完成后最后再设置一次ItemsSource,保证最终列表完整。statusText的更新放在主线程,避免操作非线程安全的控件。
5.4 ZXing.Net主包与Binding包版本不匹配
现象:编译阶段一切正常,运行到BarcodeWriter<BitmapSource>初始化时抛异常,提示找不到BitmapSourceRenderer或相关类型。
原因:ZXing.Net和ZXing.Net.Bindings.Windows.Presentation两个包版本不一致,主包升级后不再注册对应的渲染器,而Binding包还停留在旧版本。
解决:把两个包锁定到互相兼容的版本。我用的组合是ZXing.Net 0.16.9配ZXing.Net.Bindings.Windows.Presentation 0.16.7,在NuGet里同时升级或同时锁版本,不单独滚动更新其中一个。项目换到另一台电脑构建时,用packages.lock.json固定版本,避免别人拉代码后装到不同版本。
5.5 解码一直返回null但不报错
现象:BarcodeReader.Decode传入一张清晰的PNG二维码图片,结果始终是null,程序不抛任何异常,调试无从下手。
原因:最常见的原因是像素格式问题。含Alpha通道的PNG或灰度图直接交给ZXing时,内部LuminanceSource按每像素4字节解析,格式不符时得到的亮度数组是错乱的,导致解码器找不到定位图形。
解决:先对BitmapSource做像素格式归一化,转成Bgra32后再解码。格式转换代码如下:
private BitmapSource NormalizeToBgra32(BitmapSource source) { var converted = new FormatConvertedBitmap(); converted.BeginInit(); converted.Source = source; converted.DestinationFormat = PixelFormats.Bgra32; converted.EndInit(); return converted; }FormatConvertedBitmap是WPF内置的格式转换工具,BeginInit和EndInit之间设置源和目的格式。转换后再传给BarcodeReader,解码成功率明显提升。这个坑在图片来自截图、扫描仪、摄像头时尤其常见,来自画图程序保存的PNG反而很少遇到。
6. 进阶:把Demo沉淀成一个可复用的二维码工具类
6.1 工具类接口设计与实现
Demo跑通后,我把散落在窗口代码里的生成、识别、保存逻辑收拢到一个静态类中,按日常使用频率设计接口。这个类以后在多个项目里直接拷贝复用:
public static class QrCodeHelper { public static BitmapSource Generate(string content, int size = 300, int margin = 1, string charset = "UTF-8") { var writer = new BarcodeWriter<BitmapSource> { Format = BarcodeFormat.QR_CODE, Options = new QrCodeEncodingOptions { Width = size, Height = size, Margin = margin, CharacterSet = charset, ErrorCorrection = ErrorCorrectionLevel.M } }; return writer.Write(content); } public static string Decode(BitmapSource source, bool tryHarder = true) { var reader = new BarcodeReader { Options = new DecodingOptions { TryHarder = tryHarder, PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.QR_CODE } } }; var result = reader.Decode(source); return result?.Text ?? string.Empty; } public static void Save(BitmapSource image, string filePath) { string directory = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(image)); using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write); encoder.Save(stream); } }设计说明:Generate中的size、margin、charset全部带默认值,日常调用只需要传内容字符串。Decode默认开启TryHarder,保证首次识别成功率;在需要追求速度的场景,比如实时扫码,调用方可以显式传false。Save内部自动创建目录,减少调用方的流程负担。
6.2 批量验证:用数据确认工具类的稳定性
工具类封装完成后,我做了一轮批量验证:用随机内容生成500张二维码,保存后再逐张解码,统计成功率。结果如下:
| 批次 | 生成500张耗时 | 成功识别数 | 平均单张识别耗时 |
|---|---|---|---|
| 第1批 | 3.8秒 | 500 | 12ms |
| 第2批 | 4.1秒 | 500 | 13ms |
| 第3批 | 3.9秒 | 499 | 12ms |
从数据看,纯生成加识别的链路本身很稳定。失败的那一张定位后发现是保存路径包含特殊字符导致文件流写入异常,与二维码逻辑无关。这次验证让我固定了一个工作习惯:凡是交付二维码功能,先用500张批量数据跑一遍成功率,接近100%才进UI联调。这个习惯帮我避免了好几次“明明Demo能跑,一上真实数据就翻车”的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取