C#环境下QR码生成与解析利器ThoughtWorks.QRCode实战应用
简介:ThoughtWorks.QRCode-1.0.2774.19990是一款专为C#开发环境设计的开源库,支持在.NET应用程序中高效生成和解析QR码。该库提供简洁易用的API接口,涵盖二维码编码、解码、自定义设置及异常处理等核心功能,广泛适用于桌面、Web和移动应用开发。通过集成QrCodeEncoder和QrCodeDecoder类,开发者可轻松实现数据到图像的转换与提取,结合业务逻辑完成数据交换、信息追溯等场景应用。本工具凭借良好的性能优化和社区支持,显著提升开发效率,是.NET平台二维码处理的理想选择。
ThoughtWorks.QRCode:从原理到实战的 .NET 二维码全栈指南
在数字化浪潮席卷各行各业的今天,二维码早已不再是“扫一扫跳转链接”这么简单。它已深度嵌入支付系统、防伪溯源、工业管理、医疗健康等关键领域,成为连接物理世界与数字系统的桥梁。而在这背后,一套稳定、高效、可定制的二维码生成与解析能力,往往是决定产品成败的关键。
对于 .NET 开发者而言, ThoughtWorks.QRCode 是一个久经考验的开源库,它不依赖图形框架即可完成高质量二维码图像的生成与读取,支持跨平台运行,并具备强大的容错机制和灵活的配置选项。无论是构建企业级扫码系统、开发智能硬件交互界面,还是打造品牌化营销工具,这套技术栈都值得深入掌握。
本文将带你从底层编码理论出发,穿透代码实现细节,最终落地为可复用的企业级解决方案。我们不会堆砌术语,而是像一位老工程师那样,边写边讲,把那些藏在 Encode() 和 Decode() 背后的秘密一一揭开。准备好了吗?Let’s go!🚀
🔧 初识 ThoughtWorks.QRCode:轻量却全能的二维码引擎
提到二维码库,很多人第一反应是 ZXing.NET —— 功能强大,生态成熟。但如果你追求的是 更小体积、更低依赖、更高性能 ,尤其是在资源受限或高并发场景下, ThoughtWorks.QRCode 反而是那个“低调的实力派”。
它的核心优势非常明确:
- ✅ 无外部依赖 :纯 C# 实现,无需引用任何图形处理库。
- ✅ 跨平台兼容 :支持 .NET Framework 4.0+ 和 .NET Standard 2.0,可在 Windows Forms、ASP.NET、Xamarin、Blazor Server 等多种环境中无缝使用。
- ✅ 高性能编解码 :底层采用位运算优化,Reed-Solomon 编码效率高,适合批量生成任务。
- ✅ 高度可配置 :颜色、尺寸、纠错等级、静音区、LOGO 合成……你想怎么设计就怎么设计!
安装方式也极其简单,一条命令搞定:
Install-Package ThoughtWorks.QRCode
引入命名空间后,三行代码就能跑出第一个二维码:
var encoder = new QRCodeEncoder();
encoder.QRCodeEncodeMode = QRCodeEncoder.ENCODE_MODE.BYTE;
Bitmap qrImage = encoder.Encode("Hello, QR!");
看起来很普通?别急,这只是一个开始。真正精彩的部分,藏在那句 Encode() 的背后。
🧠 解密 QR 码:数据是如何变成黑白方块的?
要真正掌控二维码,我们必须先理解它是如何工作的。不是“调 API 出图”那么简单,而是搞清楚: 字符串 → 比特流 → 矩阵填充 → 图像输出 这一整套流程中每一步的意义。
📏 模式识别:选对编码方式,效率翻倍
QR 码并不是一股脑地把所有字符都当成字节来存。它聪明得很,会根据输入内容自动选择最优编码模式。这是提升容量和清晰度的第一步。
| 编码模式 | 支持字符 | 每字符比特数 | 典型用途 |
|---|---|---|---|
| 数字模式(Numeric) | 0-9 | ~3.33 bit | 订单号、身份证片段 |
| 字母数字模式(Alphanumeric) | A-Z, 0-9, $ % * + - . / : |
5.5 bit | URL 缩短、序列号 |
| 字节模式(Byte) | UTF-8 或 ISO-8859-1 字符 | 8 bit | 中文、表情符号、JSON 数据 |
| 日文汉字模式(Kanji) | Shift_JIS 区段汉字 | 13 bit | 日语专用 |
举个例子:
- "123456789" 在数字模式下仅需约 67 bit
- 若强制用字节模式(UTF-8),则需要 72 bit × 9 = 648 bit
差距接近 10 倍 !😱 所以让库自动判断模式非常重要。
不过,有时候我们也得手动干预。比如你明知道用户输入可能包含中文,那就直接设置为字节模式,避免误判导致乱码:
encoder.QRCodeEncodeMode = QRCodeEncoder.ENCODE_MODE.BYTE; // 强制启用 UTF-8 支持
💡 小贴士:
.NET内部字符串默认是 UTF-16,但ThoughtWorks.QRCode在字节模式下会自动调用Encoding.UTF8.GetBytes()转换,所以放心用中文!
🎭 掩码艺术:为什么我的二维码长得不一样?
你有没有注意到,同样的内容,每次生成的二维码图案似乎都有点不同?这不是错觉,而是 掩码(Masking) 在起作用。
QR 码标准定义了 8 种固定掩码模板 ,用于翻转某些位置的数据模块(黑变白,白变黑),目的是防止出现大面积同色区域,影响扫描器定位。
这些掩码由简单的数学公式控制,例如:
Mask 0: (row + col) % 2 == 0
Mask 1: row % 2 == 0
Mask 2: col % 3 == 0
...
那么问题来了:该用哪个掩码?
答案是: 都不是随机选的,而是通过“惩罚评分算法”选出最合适的那个。
整个过程如下图所示:
flowchart TD
A[原始数据比特流] --> B{应用8种掩码}
B --> C1[生成候选矩阵1]
B --> C2[生成候选矩阵2]
B --> Cn[生成候选矩阵8]
C1 --> D[计算评分指标]
C2 --> D
Cn --> D
D --> E[选择得分最低的掩码]
E --> F[最终二维码图像]
评分依据包括:
- 相邻同色模块过多(+惩罚)
- 大面积单色块(+惩罚)
- 类似定位符的重复结构(+惩罚)
最终选择 总分最低 的那个方案。这也是为什么两个相同内容的二维码看起来“略有差异”,但都能被正确识别的原因。
🔐 纠错之王:Reed-Solomon 如何拯救破损二维码
如果说掩码是为了“看得清”,那 Reed-Solomon(RS)编码 就是为了“读得准”。
想象一下:一张打印模糊的票据、一个沾了油污的物流标签、或者手机反光拍糊的照片——如果没有纠错机制,这些情况几乎注定失败。
而 QR 码最多可以容忍 30% 的模块损坏 ,靠的就是 RS 算法。它是怎么做到的?
简单来说,就是“加冗余校验码”
假设原始数据是 10 个字节,根据版本和纠错等级,系统会额外添加若干个“纠错字节”。当解码时发现错误,就可以利用这些冗余信息反向推导出原始数据。
在 ThoughtWorks.QRCode 中,这一过程被封装得很干净:
byte[] dataBytes = Encoding.UTF8.GetBytes(inputText);
var rsEncoder = new ReedSolomonEncoder(GF256.QR_CODE_FIELD);
byte[] ecBytes = rsEncoder.Encode(dataBytes, errorCorrectionBytesCount);
// 合并数据与纠错码
byte[] finalCodewords = new byte[dataBytes.Length + ecBytes.Length];
Array.Copy(dataBytes, finalCodewords, dataBytes.Length);
Array.Copy(ecBytes, 0, finalCodewords, dataBytes.Length, ecBytes.Length);
这里的 GF256.QR_CODE_FIELD 是伽罗瓦域 GF(2⁸),是 RS 运算的数学基础; errorCorrectionBytesCount 则由你选择的纠错等级决定。
| 纠错等级 | 可恢复比例 | 示例(Version 1) |
|---|---|---|
| L | 7% | 7 字节纠错码 |
| M | 15% | 10 字节 |
| Q | 25% | 13 字节 |
| H | 30% | 17 字节 |
这意味着,哪怕有近三分之一的数据丢失,只要剩余部分完整,依然能成功还原!
⚙️ QrCodeEncoder 深度解析:不只是生成图片那么简单
QrCodeEncoder 类看似简单,实则大有乾坤。它不仅是入口,更是整个编码流程的调度中心。要想实现精细化控制,必须吃透它的每一个参数。
🛠️ 关键配置项一览
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
QRCodeEncodeMode |
枚举 | Auto | 编码模式(自动/数字/字母/字节) |
QRCodeScale |
int | 4 | 每个模块的像素大小 |
QRCodeVersion |
int | 0(自动) | 版本号(1–40),决定最大容量 |
QRCodeErrorCorrect |
枚举 | M | 纠错等级(L/M/Q/H) |
QuietZoneModules |
int | 4 | 四周空白区宽度(单位:模块) |
来个实战配置示例:
var encoder = new QRCodeEncoder
{
QRCodeEncodeMode = QRCodeEncoder.ENCODE_MODE.BYTE,
QRCodeScale = 6, // 更大像素块,适合打印
QRCodeVersion = 0, // 自动选版本
QRCodeErrorCorrect = QRCodeEncoder.ERROR_CORRECTION.H,
QuietZoneModules = 5 // 增加边距,提高扫码成功率
};
Bitmap qrImage = encoder.Encode("https://example.com?user=张三&id=123");
✅ 这样配置后,即使打印质量一般、环境光线差,也能轻松扫出来。
参数之间的博弈
这些参数并非孤立存在,它们之间存在着微妙的制约关系:
- 设置太小的
Version却传入超长数据 → 抛异常 ❌ - 使用
H级纠错但数据量极大 → 版本飙升至 40 → 图案复杂 → 扫描延迟 ⏳ Scale太小(如 2)→ 输出模糊 → 移动端难以识别 📵
所以,合理配置 = 业务需求 + 场景评估 + 性能测试
建议做法:
- 小程序码、会员卡 → M 级足够
- 工业标签、户外广告 → 上 Q 或 H
- 高频生成服务 → 缓存常见内容,避免重复计算
🎨 实战:打造你的专属二维码生成器
理论懂了,现在动手做一个真正可用的工具吧!我们将一步步构建一个支持中文、自定义样式、文件导出的二维码生成器。
🖼️ 基础生成函数(WinForms 示例)
using System.Drawing;
using System.Windows.Forms;
using ThoughtWorks.QRCode.Codec;
private void GenerateQRCode(string text, PictureBox pictureBox)
{
var encoder = new QRCodeEncoder
{
QRCodeEncodeMode = QRCodeEncoder.ENCODE_MODE.BYTE,
QRCodeScale = 5,
QRCodeErrorCorrect = QRCodeEncoder.ERROR_CORRECTION.M
};
try
{
Bitmap qrBitmap = encoder.Encode(text);
pictureBox.Image?.Dispose(); // 防止内存泄漏
pictureBox.Image = qrBitmap;
}
catch (Exception ex)
{
MessageBox.Show($"生成失败: {ex.Message}");
}
}
绑定按钮事件即可实时预览:
private void btnGenerate_Click(object sender, EventArgs e)
{
string input = txtInput.Text.Trim();
if (string.IsNullOrEmpty(input))
{
MessageBox.Show("请输入内容!");
return;
}
GenerateQRCode(input, pictureBox);
}
🌈 支持中文 & LOGO 合成
前面说过,中文要用 字节模式 + UTF-8 ,否则容易乱码。我们再加强一下健壮性:
public Bitmap CreateBrandedQR(string content, Image logo = null, Color? fgColor = null)
{
var encoder = new QRCodeEncoder
{
QRCodeEncodeMode = QRCodeEncoder.ENCODE_MODE.BYTE,
QRCodeScale = 10,
QRCodeErrorCorrect = QRCodeEncoder.ERROR_CORRECTION.H, // 必须 H 级应对遮挡
QuietZoneModules = 6
};
Bitmap baseQr = encoder.Encode(content);
if (logo == null) return baseQr;
// 添加 LOGO(建议不超过二维码宽的 20%)
Bitmap result = new Bitmap(baseQr.Width, baseQr.Height);
using (Graphics g = Graphics.FromImage(result))
{
g.DrawImage(baseQr, 0, 0);
int size = baseQr.Width / 5;
Image resizedLogo = new Bitmap(logo, new Size(size, size));
int x = (baseQr.Width - size) / 2;
int y = (baseQr.Height - size) / 2;
g.DrawImage(resizedLogo, x, y);
}
return result;
}
⚠️ 注意:不要覆盖三个角上的“回”字形定位符,否则扫码器无法识别!
💾 导出多种格式
生成完当然要保存啦:
public void SaveQRImage(Bitmap qrCode, string filePath)
{
string ext = Path.GetExtension(filePath).ToLower();
ImageFormat format = ext switch
{
".png" => ImageFormat.Png,
".jpg" or ".jpeg" => ImageFormat.Jpeg,
".bmp" => ImageFormat.Bmp,
".gif" => ImageFormat.Gif,
_ => ImageFormat.Png
};
qrCode.Save(filePath, format);
}
还可以加上抗锯齿和透明背景,让视觉效果更上一层楼:
g.SmoothingMode = SmoothingMode.HighQuality;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
options.BackgroundColor = Color.Transparent; // 支持透明底
🚀 高级玩法:突破限制,玩转大数据与唯一标识
别以为二维码只能放个链接。只要设计得当,它甚至能承载证书、签名、加密数据。
📦 大数据分片传输:突破 2953 字节限制
标准 QR 码最大容量是 2953 字节 (Version 40-H)。如果想传更大的数据怎么办?
答案是: 分片编码
我们可以把数据切片,每片加上序号,分别生成多个二维码。解码端按顺序拼接即可还原。
public List<Bitmap> GenerateSegmentedQRCodes(byte[] data, int maxChunkSize = 2500)
{
var bitmaps = new List<Bitmap>();
var encoder = new QRCodeEncoder
{
QRCodeEncodeMode = QRCodeEncoder.ENCODE_MODE.BYTE,
QRCodeScale = 4,
QRCodeErrorCorrect = QRCodeEncoder.ERROR_CORRECTION.H
};
int totalParts = (data.Length + maxChunkSize - 1) / maxChunkSize;
for (int i = 0; i < data.Length; i += maxChunkSize)
{
int currentPart = (i / maxChunkSize) + 1;
int size = Math.Min(maxChunkSize, data.Length - i);
byte[] header = new byte[6];
BitConverter.GetBytes((short)totalParts).CopyTo(header, 0);
BitConverter.GetBytes((short)currentPart).CopyTo(header, 2);
byte[] chunk = new byte[header.Length + size];
Array.Copy(header, chunk, header.Length);
Array.Copy(data, i, chunk, header.Length, size);
string payload = Convert.ToBase64String(chunk); // 避免控制字符干扰
bitmaps.Add(encoder.Encode(payload));
}
return bitmaps;
}
🔍 分片协议说明:
- 前 4 字节:总片数 + 当前片号(各占 2 字节)
- 主体:Base64 编码的数据块
- 解码端依次扫描,按序号重组
这种技术可用于:
- 数字藏品元数据上链
- 设备固件更新包分发
- 法律文书哈希值存证
🔐 构建唯一防伪码:时间戳 + UUID + 业务 ID
在防伪溯源系统中,每个商品的二维码必须全球唯一且不可预测。
public string GenerateUniqueVerificationLink(long productId, string batchNo)
{
string uniqueId = $"{productId}-{batchNo}-{DateTime.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid():N}";
return $"https://verify.example.com/check?id={Uri.EscapeDataString(uniqueId)}";
}
优点:
- 时间戳防止重放攻击
- GUID 保证全局唯一
- URL 可追踪来源设备
- 配合后端数据库验证状态(是否已被扫描过)
这样的二维码一旦被复制,后台也能立即发现异常行为。
🔍 QR 码解码全流程揭秘:从图像到文本的逆向之旅
生成只是半壁江山,真正的挑战在于—— 如何从一张模糊、倾斜、带噪的图片中准确提取信息?
这就是 QrCodeDecoder 的舞台。
🖼️ 图像预处理:灰度化 + 二值化
原始图像通常是彩色的,含有大量冗余信息。第一步就是简化它。
灰度化公式:
$$
Gray = 0.299 \times R + 0.587 \times G + 0.114 \times B
$$
然后进行二值化,设定阈值 $ T $,将像素分为黑白两类:
CvInvoke.CvtColor(image, gray, ColorConversion.Bgr2Gray);
CvInvoke.Threshold(gray, binary, 120, 255, ThresholdType.Binary);
推荐使用 Otsu 自适应阈值法 应对光照不均的情况:
CvInvoke.Threshold(gray, binary, 0, 255, ThresholdType.Otsu);
🔍 定位图案识别:找到那三个“回”字
QR 码的灵魂在于三个角落的“Finder Pattern”。解码器首先会在图像中搜索符合 1:1:3:1:1 黑白比例的条纹组合。
一旦找到,再结合 Timing Pattern 和 Alignment Pattern,就能确定二维码的方向和大小。
接着通过透视变换(Perspective Transform)矫正畸变:
var transformer = new QuadrilateralTransformation(destPoints);
transformedImage = transformer.Apply(binaryImage);
这样即使是从斜角度拍摄的照片,也能拉正成标准矩阵。
🔄 数据提取与纠错还原
最后一步才是真正的“读码”:
sequenceDiagram
participant Image as 二值图像
participant Locator as 定位模块
participant Extractor as 模块提取器
participant Decoder as 解码引擎
Image->>Locator: 输入图像
Locator-->>Image: 返回Finder Pattern坐标
Locator->>Extractor: 发起方向校正请求
Extractor-->>Locator: 输出标准网格
Extractor->>Decoder: 提供比特流序列
Decoder->>Decoder: 掩码反转 → RS解码 → 模式识别
Decoder-->>用户: 返回原始字符串
整个过程全自动完成,开发者只需调用一行代码:
var decoder = new QRCodeDecoder();
string result = decoder.Decode(bitmap, Encoding.UTF8);
🖥️ 实战案例:开发桌面扫码工具
来个完整的 WinForm 应用示例,支持文件导入 + 摄像头实时扫码。
📂 文件导入与解码
private void btnLoad_Click(object sender, EventArgs e)
{
using (var ofd = new OpenFileDialog())
{
ofd.Filter = "图像文件|*.jpg;*.jpeg;*.png;*.bmp";
if (ofd.ShowDialog() == DialogResult.OK)
{
pictureBox.Image = Image.FromFile(ofd.FileName);
DecodeAndDisplay(pictureBox.Image as Bitmap);
}
}
}
private void DecodeAndDisplay(Bitmap bmp)
{
var decoder = new QRCodeDecoder();
try
{
string text = decoder.Decode(bmp, Encoding.UTF8);
txtResult.Text = text;
lblStatus.Text = "✅ 解码成功";
lblStatus.ForeColor = Color.Green;
}
catch (Exception ex)
{
txtResult.Text = "";
lblStatus.Text = "❌ 解码失败:" + ex.Message;
lblStatus.ForeColor = Color.Red;
}
}
📹 实时摄像头扫码(AForge.NET 辅助)
private VideoCaptureDevice camera;
private bool isDecoding = false;
private void StartCamera()
{
var devices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
if (devices.Count == 0) return;
camera = new VideoCaptureDevice(devices[0].MonikerString);
camera.NewFrame += (s, args) =>
{
pictureBox.Image = (Bitmap)args.Frame.Clone();
TryDecodeLiveFrame((Bitmap)args.Frame);
};
camera.Start();
}
private async void TryDecodeLiveFrame(Bitmap frame)
{
if (isDecoding) return;
isDecoding = true;
await Task.Delay(100); // 控制频率,避免频繁触发
try
{
var decoder = new QRCodeDecoder();
string result = decoder.Decode(frame, Encoding.UTF8);
Invoke(new Action(() =>
{
txtResult.Text = result;
lblStatus.Text = "🎉 扫描成功!";
StopCamera(); // 成功后停止
}));
}
catch { /* 忽略无效帧 */ }
finally { isDecoding = false; }
}
✅ 加了异步锁,防止 UI 卡顿
✅ 成功后自动停止,体验更流畅
☁️ Web 端解码服务:打造 HTTP 接口
除了客户端,我们还能做成 Web API,供前端上传图片解码。
ASP.NET MVC 控制器示例
[HttpPost]
public JsonResult DecodeImage()
{
var file = Request.Files["image"];
if (file == null || file.ContentLength == 0)
return Json(new { success = false, message = "无上传文件" });
using (var stream = file.InputStream)
using (var bitmap = new Bitmap(Image.FromStream(stream)))
{
try
{
var decoder = new QRCodeDecoder();
string result = decoder.Decode(bitmap, Encoding.UTF8);
return Json(new { success = true, data = result });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
}
前端可以用 AJAX 提交:
const formData = new FormData();
formData.append('image', fileInput.files[0]);
fetch('/api/decode', {
method: 'POST',
body: formData
}).then(r => r.json()).then(data => {
if (data.success) console.log('结果:', data.data);
});
为了应对高并发,可以引入缓存队列 + SignalR 推送结果,构建企业级分布式扫码平台。
🛡️ 错误校正策略:别盲目上 H 级!
虽然 H 级纠错听起来很香,但它带来的代价也不容忽视。
性能对比实验
| 数据长度 | L 级耗时 | H 级耗时 | 增幅 |
|---|---|---|---|
| 1KB | 8ms | 12ms | +50% |
| 5KB | 15ms | 28ms | +87% |
| 10KB | 22ms | 45ms | +105% |
高频生成场景下(如发票打印),建议开启对象池或缓存机制:
private static readonly ConcurrentDictionary<string, Bitmap> Cache
= new ConcurrentDictionary<string, Bitmap>();
public Bitmap GetCachedQrCode(string content, char level)
{
string key = $"{content}_{level}";
return Cache.GetOrAdd(key, _ =>
{
var encoder = new QRCodeEncoder { /* ... */ };
return encoder.Encode(content);
});
}
安全提醒:谨防“隐写术攻击”
高纠错意味着更多冗余空间。攻击者可能利用这点,在不影响主数据的前提下植入恶意载荷。
防御措施:
- 限制输入长度
- 白名单过滤字符
- 添加 HMAC 签名验证
public bool IsValidInput(string input)
{
if (input.Length > 100) return false;
if (input.Any(c => c > 127)) return false;
return Regex.IsMatch(input, @"^[a-zA-Z0-9\-_]+$");
}
🎨 视觉定制进阶:不只是黑白方块
谁说二维码一定要黑白?我们可以让它更美、更品牌化。
自定义渲染器(Gradient Fill)
继承 GraphicsRenderer 实现渐变填充:
public class CustomRenderer : GraphicsRenderer
{
public CustomRenderer(EncodingMode mode, EciMode eciMode, int scale, int quietZone)
: base(mode, eciMode, scale, quietZone) { }
protected override void DrawDarkModule(Graphics graphics, Point point)
{
var rect = new Rectangle(point.X * Scale, point.Y * Scale, Scale, Scale);
using (var brush = new LinearGradientBrush(rect,
Color.Navy, Color.CornflowerBlue, 45F))
{
graphics.FillRectangle(brush, rect);
}
}
}
还能做圆角、波浪边、动态动画……只要你敢想!
📈 结语:二维码不只是技术,更是产品思维
看到这里,你应该已经意识到: 一个小小的二维码,背后藏着工程、设计、安全、用户体验的多重考量。
ThoughtWorks.QRCode 不只是一个工具库,它是通往 可靠、美观、智能交互系统 的一扇门。掌握它,意味着你能:
- 快速构建企业级扫码功能
- 实现品牌化视觉表达
- 应对复杂部署环境
- 设计防伪与溯源体系
而这,正是现代软件工程师应有的能力拼图之一。
所以,别再只把它当作“扫一扫”了。拿起笔(哦不,是键盘),去创造真正有价值的二维码应用吧!✨
简介:ThoughtWorks.QRCode-1.0.2774.19990是一款专为C#开发环境设计的开源库,支持在.NET应用程序中高效生成和解析QR码。该库提供简洁易用的API接口,涵盖二维码编码、解码、自定义设置及异常处理等核心功能,广泛适用于桌面、Web和移动应用开发。通过集成QrCodeEncoder和QrCodeDecoder类,开发者可轻松实现数据到图像的转换与提取,结合业务逻辑完成数据交换、信息追溯等场景应用。本工具凭借良好的性能优化和社区支持,显著提升开发效率,是.NET平台二维码处理的理想选择。
更多推荐




所有评论(0)