一、需求分析與方案設計
背景問題
在車水馬龍的現代都市,停車難題如影隨形,挪車也成為廣大車主們日常面臨的困擾之一。過去,為了方便他人聯繫挪車,大家常常會在車內顯眼位置放置寫有電話號碼的紙條,或是擺放刻有手機號碼的挪車擺件。這種傳統方式看似簡單直接,卻藏著不少隱患。
首先是隱私泄露問題,如今個人信息安全至關重要,在公共場所隨意暴露電話號碼,就如同將自己的隱私大門敞開。不法分子會利用這些信息進行惡意騷擾,推銷電話、詐騙短信接踵而至,讓人不堪其擾。曾經就有新聞報導,某些"抄號族"穿梭於停車場,專門收集挪車電話,再將這些信息批量出售給營銷公司或詐騙團伙,導致車主們的生活被各種垃圾信息充斥。
除了隱私泄露,挪車號碼被濫用的情況也屢見不鮮。有時明明只是臨時停車一小會兒,卻頻繁接到挪車電話,甚至還有人在非緊急情況下隨意撥打,耽誤車主時間。更嚴重的是,有心懷不軌之人可能利用挪車電話實施詐騙,以車輛違章、事故等虛假理由,誘導車主點擊連結或轉帳,致使車主遭受財產損失。
解決方案
為了解決上述問題,我們開發了一個簡單實用的挪車二維碼生成工具,實現以下功能:
- 快速生成包含車主聯繫方式的二維碼
- 通過掃碼即可一鍵撥打車主電話
- 支持自定義挪車提示文本
- 手機號碼經過加密處理,保護車主隱私
- 提供離線桌面版和在線網頁版兩種使用方式
與傳統的直接展示電話號碼不同,我們的挪車二維碼工具採用了加密技術,確保車主的手機號不會直接暴露。同時,通過掃碼跳轉專門的挪車頁面,不僅提升了使用體驗,也降低了號碼被濫用的風險。本文將詳細居間這兩種實現方式:基於c#和avalonia的桌面應用版本,以及基於blazor前端和.net web api的在線網頁版本。
效果如下,微信掃碼彈出詳細頁面:
![]() |
![]() |
在詳細頁面,可點擊撥打車主電話或點擊綠色超連結去生成一個挪車碼:
![]() |
![]() |
![]() |
二、核心二維碼生成代碼
首先,让我们看一下核心的二维码生成逻辑。这部分代码封装在CodeWF.Tools项目的QrCodeGenerator类中:
using ImageMagick;
using ImageMagick.Drawing;
using ZXing;
using ZXing.QrCode;
namespace CodeWF.Tools.Image;
public static class QrCodeGenerator
{
public static void GenerateQrCode(string title, string content, string imagePath, string? subTitle = "")
{
var qrCodeWriter = new BarcodeWriterPixelData
{
Format = BarcodeFormat.QR_CODE,
Options = new QrCodeEncodingOptions
{
Width = 400,
Height = 400,
Margin = 1,
ErrorCorrection = ZXing.QrCode.Internal.ErrorCorrectionLevel.H,
CharacterSet = "UTF-8",
DisableECI = true
}
};
var pixelData = qrCodeWriter.Write(content);
using var qrCodeImage = new MagickImage();
var settings = new PixelReadSettings((uint)pixelData.Width, (uint)pixelData.Height, StorageType.Char, PixelMapping.RGBA);
qrCodeImage.ReadPixels(pixelData.Pixels, settings);
var backgroundHeight = string.IsNullOrWhiteSpace(subTitle) ? 600u : 630u;
using var background = new MagickImage(MagickColors.White, 500, backgroundHeight);
background.BorderColor = new MagickColor("#2888E2");
background.Border(8);
var titleText = new Drawables()
.Font("SimHei")
.FontPointSize(95)
.FillColor(new MagickColor("#FF5722"))
.TextAlignment(TextAlignment.Center)
.Text(250, 120, title);
background.Draw(titleText);
background.Composite(qrCodeImage, 50, 170, CompositeOperator.Over);
if (!string.IsNullOrWhiteSpace(subTitle))
{
var subTitleText = new Drawables()
.Font("SimHei")
.FontPointSize(20)
.FillColor(new MagickColor("#333333"))
.TextAlignment(TextAlignment.Center)
.Text(250, 600, subTitle);
background.Draw(subTitleText);
}
//using var logo = new MagickImage("logo.png");
//logo.Resize(100, 100);
//background.Composite(logo, 250, 250, CompositeOperator.Over);
background.Quality = 100;
background.Write(imagePath);
}
}
上面代碼使用了nuget包:zxing.net.bindings.magick。zxing.net用於生成二維碼矩陣數據,而magick.net則用於圖像處理和合成,實現了以下功能:
- 創建高質量的qr碼,設置了較高的糾錯級別(h級別),即使二維碼部分被遮擋也能正常識別
- 生成一個白底藍邊的背景圖片
- 在頂部添加自定義的標題文本
- 將二維碼圖像合成到背景上
- 在二維碼底部可添加自定義的小標題文本
- 保存為高質量png圖像
這種設計讓最終生成的挪車二維碼既美觀又實用。

三、離線桌面版實現
1. 用戶界面設計
使用Avalonia框架设计用户界面,界面定义在NuoCheView.axaml文件中:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:CodeWF.Modules.Converter.ViewModels"
xmlns:prism="http://prismlibrary.com/"
xmlns:u="https://irihi.tech/ursa"
xmlns:i18n="https://codewf.com"
xmlns:language="clr-namespace:Localization"
prism:ViewModelLocator.AutoWireViewModel="True"
x:CompileBindings="True"
x:DataType="vm:NuoCheViewModel"
d:DesignHeight="450"
d:DesignWidth="800"
x:Class="CodeWF.Modules.Converter.Views.NuoCheView"
mc:Ignorable="d">
<StackPanel Margin="10" HorizontalAlignment="Center">
<u:Form LabelPosition="Left" LabelWidth="*">
<TextBox Width="300" u:FormItem.Label="{i18n:I18n {x:Static language:NuoCheView.InputPhoneNumber}}"
u:FormItem.IsRequired="True" Text="{Binding PhoneNumber}" />
<TextBox Width="300" u:FormItem.Label="{i18n:I18n {x:Static language:NuoCheView.InputTitle}}"
u:FormItem.IsRequired="True" Text="{Binding InputTitle}" />
<StackPanel Orientation="Horizontal" u:FormItem.Label="{i18n:I18n {x:Static language:NuoCheView.EnableSubTitle}}">
<CheckBox IsChecked="{Binding EnableSubTitle}" Command="{Binding EnableSubTitleHandler}" VerticalAlignment="Center" Content="显示" Margin="5 0" />
<TextBox Width="300" Text="{Binding SubTitle}" IsEnabled="{Binding EnableSubTitle}" VerticalAlignment="Center" />
</StackPanel>
</u:Form>
<StackPanel Orientation="Horizontal" Spacing="10" Margin="0,0,0,10">
<Button Content="{i18n:I18n {x:Static language:NuoCheView.CreateButtonContent}}"
Command="{Binding GenerateQrCode}" />
<Button Content="{i18n:I18n {x:Static language:NuoCheView.SaveQrCodeFileTitle}}"
Command="{Binding SaveQrCode}" />
<HyperlinkButton Content="{i18n:I18n {x:Static language:NuoCheView.PreviewNuoCheUrl}}"
NavigateUri="{Binding GeneratedUrl}" Classes="WithIcon Underline"
VerticalAlignment="Center" />
</StackPanel>
<Image Source="{Binding QrCodeImage}" Width="340" Height="340" DragDrop.AllowDrop="True">
<Interaction.Behaviors>
<EventTriggerBehavior EventName="PointerPressed">
<InvokeCommandAction Command="{Binding RaisePointerPressed}" PassEventArgsToCommand="True" />
</EventTriggerBehavior>
</Interaction.Behaviors>
</Image>
</StackPanel>
</UserControl>
界面布局簡潔明了,主要包括:
- 三個輸入框:手機號碼和二維碼標題、可選二維碼小標題
- 三個操作按鈕:生成二維碼、保存二維碼和預覽挪車頁面
- 一個圖像顯示區域:展示生成的二維碼,並支持拖拽操作
實現效果如下:

2. 視圖模型實現
在NuoCheViewModel.cs中实现业务逻辑:
using Avalonia.Input;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using AvaloniaXmlTranslator;
using CodeWF.Core.IServices;
using CodeWF.LogViewer.Avalonia;
using CodeWF.Tools.Image;
using HashidsNet;
using ReactiveUI;
namespace CodeWF.Modules.Converter.ViewModels;
public class NuoCheViewModel : ReactiveObject
{
private readonly INotificationService _notificationService;
private readonly IFileChooserService _fileChooserService;
private string _inputTitle;
private long _phoneNumber;
private Bitmap? _qrCodeImage;
private string _qrCodeImagePath;
private string _generatedUrl;
private string _subTitle;
private bool _enableSubTitle;
public NuoCheViewModel(INotificationService notificationService, IFileChooserService fileChooserService)
{
_notificationService = notificationService;
_fileChooserService = fileChooserService;
InputTitle = I18nManager.Instance.GetResource(Localization.NuoCheView.DefaultInputTitle);
PhoneNumber = 16899999999;
SubTitle = $"{I18nManager.Instance.GetResource(Localization.NuoCheView.DefaultSubTitlePrefix)}: {PhoneNumber}";
EnableSubTitle = false;
}
public string InputTitle
{
get => _inputTitle;
set => this.RaiseAndSetIfChanged(ref _inputTitle, value);
}
public long PhoneNumber
{
get => _phoneNumber;
set => this.RaiseAndSetIfChanged(ref _phoneNumber, value);
}
public Bitmap? QrCodeImage
{
get => _qrCodeImage;
private set => this.RaiseAndSetIfChanged(ref _qrCodeImage, value);
}
public string QrCodeImagePath
{
get => _qrCodeImagePath;
private set => this.RaiseAndSetIfChanged(ref _qrCodeImagePath, value);
}
public string GeneratedUrl
{
get => _generatedUrl;
private set => this.RaiseAndSetIfChanged(ref _generatedUrl, value);
}
public string SubTitle
{
get => _subTitle;
set => this.RaiseAndSetIfChanged(ref _subTitle, value);
}
public bool EnableSubTitle
{
get => _enableSubTitle;
set => this.RaiseAndSetIfChanged(ref _enableSubTitle, value);
}
public void EnableSubTitleHandler()
{
SubTitle = $"{I18nManager.Instance.GetResource(Localization.NuoCheView.DefaultSubTitlePrefix)}: {PhoneNumber}";
}
public void GenerateQrCode()
{
if (string.IsNullOrWhiteSpace(InputTitle))
{
_notificationService.Show(I18nManager.Instance.GetResource(Localization.NuoCheView.Title),
I18nManager.Instance.GetResource(Localization.NuoCheView.NeedInputTip));
return;
}
try
{
var encodedPhone = new Hashids("codewf").EncodeLong(PhoneNumber);
GeneratedUrl =
$"https://codewf.com/nuoche?p={encodedPhone}";
QrCodeImagePath = Path.Combine(Path.GetTempPath(), "nuoche.png");
QrCodeGenerator.GenerateQrCode(InputTitle, GeneratedUrl, QrCodeImagePath,
EnableSubTitle ? SubTitle : null);
QrCodeImage = new Bitmap(QrCodeImagePath);
}
catch (Exception ex)
{
Logger.Error(I18nManager.Instance.GetResource(Localization.NuoCheView.CreateErrorMessage), ex);
_notificationService.Show(I18nManager.Instance.GetResource(Localization.NuoCheView.Title),
$"{I18nManager.Instance.GetResource(Localization.NuoCheView.CreateErrorMessage)}: {ex}");
}
}
public async Task SaveQrCode()
{
if (QrCodeImage == null || string.IsNullOrEmpty(QrCodeImagePath))
{
_notificationService.Show(I18nManager.Instance.GetResource(Localization.NuoCheView.SaveNotificationTitle),
I18nManager.Instance.GetResource(Localization.NuoCheView.SaveNoQrCodeMessage));
return;
}
try
{
var file = await _fileChooserService.SaveFileAsync(
I18nManager.Instance.GetResource(Localization.NuoCheView.SaveQrCodeFileTitle),
new[]
{
new FilePickerFileType(
I18nManager.Instance.GetResource(Localization.NuoCheView.SaveQrCodeFileFormat))
{
Patterns = new[] { "*.png" }
}
}
);
if (file != null)
{
File.Copy(QrCodeImagePath, file, true);
_notificationService.Show(
I18nManager.Instance.GetResource(Localization.NuoCheView.SaveQrCodeSuccessTitle),
I18nManager.Instance.GetResource(Localization.NuoCheView.SaveQrCodeSuccessMessage));
}
}
catch (Exception ex)
{
_notificationService.Show(I18nManager.Instance.GetResource(Localization.NuoCheView.SaveNotificationTitle),
$"{I18nManager.Instance.GetResource(Localization.NuoCheView.SaveQrCodeErrorMessage)}: {ex.Message}");
}
}
public async Task RaisePointerPressed(PointerPressedEventArgs e)
{
if (string.IsNullOrWhiteSpace(QrCodeImagePath) || !File.Exists(QrCodeImagePath))
{
return;
}
var dragData = new DataObject();
if (await _fileChooserService.StorageProvider.TryGetFileFromPathAsync(QrCodeImagePath) is { } storageFile)
{
dragData.Set(DataFormats.Files, new[] { storageFile });
}
await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy);
}
}
視圖模型實現了以下功能:
- 數據綁定:管理用戶輸入和ui狀態
- 二維碼生成:調用核心生成邏輯並顯示結果
- 文件保存:將生成的二維碼保存到用戶指定位置
- 拖拽支持:允許用戶直接拖拽二維碼圖像到其他應用
- 錯誤處理:提供友好的錯誤提示
值得一提的是,我们使用了Hashids库对手机号进行编码,提高了隐私保护。这样,即使二维码被公开展示,他人也无法直接获取车主的真实手机号(当然最后一步进入拨号界面时会显示真实手机号,这里可考虑使用虚拟号码屏蔽真实手机号)。
拖拽保存挪車二維碼效果:

四、在線網頁版實現
除了桌面應用版本外,我們還開發了一個基於blazor的在線挪車二維碼生成工具,方便用戶無需安裝軟體即可使用。
在線訪問地址:https://dotnet9.com/nuoche
pc端創建挪車碼:

手機端創建挪車碼:

1. 在線生成挪車碼特點
與桌面版相比,在線版本有以下特點:
- 無需安裝:直接通過瀏覽器訪問使用
- 雙向功能:既可以生成二維碼,也可以直接訪問解析後的挪車頁面
- 移動端友好:優化了行動裝置上的顯示和交互體驗
- 優雅的ui設計:使用現代化的ui組件和交互效果
2. 實現方式
在線版本使用blazor實現,核心代碼結構如下:
@page "/NuoChe"
@using HashidsNet
@inject IOptions<SiteOption> SiteOption
@layout EmptyLayout
@code {
[SupplyParameterFromQuery]
public string? P { get; set; }
public const string Slug = "nuoche";
private long? _decodePhone;
protected override void OnInitialized()
{
base.OnInitialized();
if (!string.IsNullOrWhiteSpace(P))
{
_decodePhone = new Hashids("codewf").DecodeLong(P)[0];
}
}
}
<PageTitle>免费挪车码</PageTitle>
<div class="nuoche-container">
@if (string.IsNullOrWhiteSpace(P))
{
<div class="generator-container">
<h1 class="generator-title">挪车码在线生成器</h1>
<div class="input-group">
<label for="phoneNumber">手机号码</label>
<input type="tel" id="phoneNumber" placeholder="请输入手机号码" />
</div>
<div class="input-group">
<label for="title">标题</label>
<input type="text" id="title" placeholder="扫码挪车" value="扫码挪车" />
</div>
<div class="input-group">
<div class="checkbox-container">
<input type="checkbox" id="enableSubtitle" />
<label for="enableSubtitle">添加小标题</label>
</div>
<input type="text" id="subtitle" placeholder="扫码联系车主或拨打电话: 16800000000" disabled />
</div>
<div class="button-group">
<button class="primary-button" onclick="generateQrCode()">
<i class="fas fa-qrcode"></i> 生成二维码
</button>
</div>
<div class="qr-code-container" style="display: none">
<img alt="挪车码" />
<div class="action-buttons">
<a target="_blank" class="preview-link">
<i class="fas fa-external-link-alt"></i> 预览
</a>
<a class="download-link">
<i class="fas fa-download"></i> 下载
</a>
</div>
<div class="alert alert-warning mt-2">
<i class="fas fa-clock"></i> 请注意:生成的文件仅保留2分钟,请及时下载。
</div>
</div>
</div>
}
else
{
<div class="card">
<div class="card-header">
<i class="fas fa-car"></i>
<h1 class="title">临时停靠,请多关照</h1>
</div>
<div class="card-body">
<p class="description">如果我的车阻碍了您的车辆通行,点击下方按钮通知我,给您带来不便敬请谅解。</p>
<a class="phone-button" href="tel:@_decodePhone">
<i class="fas fa-phone-alt"></i> 拨打车主电话
</a>
<a class="generate-link" href="/nuoche">去生成一个挪车码</a>
</div>
</div>
}
</div>
該實現有兩個主要功能頁面:
- 生成頁面:當用戶直接訪問/nuoche時,顯示二維碼生成界面,允許輸入手機號和標題
- 挪車頁面:當用戶通過二維碼掃描訪問(帶有p參數)時,顯示一鍵撥打車主電話的界面
通過這種設計,我們實現了二維碼的完整生命周期:生成、掃描、聯繫,為用戶提供了便捷的挪車解決方案。
前面截圖掃碼後顯示詳細信息頁面:

3. 在線版本核心javascript交互
async function generateQrCode() {
const title = document.getElementById('title').value;
const phoneNumber = document.getElementById('phoneNumber').value;
const enableSubtitle = document.getElementById('enableSubtitle').checked;
let subtitle = null;
if (enableSubtitle) {
subtitle = document.getElementById('subtitle').value.trim();
if (!subtitle) {
subtitle = `扫码联系车主或拨打电话: ${phoneNumber || "16800000000"}`;
document.getElementById('subtitle').value = subtitle;
}
}
if (!title || !phoneNumber) {
alert('请输入标题和手机号码');
return;
}
try {
const response = await fetch('/api/Image/nuoche', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: title,
phoneNumber: phoneNumber,
subtitle: subtitle
})
});
const data = await response.json();
if (data.success) {
const qrCodeContainer = document.querySelector('.qr-code-container');
const img = qrCodeContainer.querySelector('img');
img.src = data.qrCodeUrl;
qrCodeContainer.querySelector('.preview-link').href = data.generatedUrl;
// 添加下载功能
const downloadLink = qrCodeContainer.querySelector('.download-link');
downloadLink.onclick = () => {
const a = document.createElement('a');
a.href = data.qrCodeUrl;
a.download = '挪车码.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
qrCodeContainer.style.display = 'block';
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('生成二维码失败:', error);
alert('生成二维码失败,请稍后重试');
}
}
// 添加小标题复选框的事件处理
document.addEventListener('DOMContentLoaded', function() {
const enableSubtitleCheckbox = document.getElementById('enableSubtitle');
const subtitleInput = document.getElementById('subtitle');
const phoneInput = document.getElementById('phoneNumber');
if (enableSubtitleCheckbox && subtitleInput) {
enableSubtitleCheckbox.addEventListener('change', function() {
subtitleInput.disabled = !this.checked;
if (this.checked) {
if (!subtitleInput.value.trim()) {
const phoneNumber = phoneInput.value.trim() || "16800000000";
subtitleInput.value = `扫码联系车主或拨打电话: ${phoneNumber}`;
}
subtitleInput.focus();
}
});
// 当手机号码变化时,如果小标题已启用且使用的是默认格式,则更新小标题中的电话号码
phoneInput.addEventListener('input', function() {
if (enableSubtitleCheckbox.checked && subtitleInput.value.startsWith('扫码联系车主或拨打电话:')) {
const phoneNumber = this.value.trim() || "16800000000";
subtitleInput.value = `扫码联系车主或拨打电话: ${phoneNumber}`;
}
});
}
});
在线版本的后端API实现与离线版类似,都是使用相同的核心二维码生成逻辑,但增加了文件上传处理、临时存储和清理等功能,这里不再细述,在线版源码戳这,挪车二维码接口定义如下:
[HttpPost("nuoche")]
[AllowAnonymous]
public async Task<IActionResult> NuoCheAsync([FromBody] NuoCheRequest request,
[FromServices] IWebHostEnvironment env,
[FromServices] IOptions<SiteOption> siteOption)
{
try
{
if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.PhoneNumber))
{
return BadRequest(new { success = false, message = "标题和手机号码不能为空" });
}
if (!long.TryParse(request.PhoneNumber, out long phoneNumberLong))
{
return BadRequest(new { success = false, message = "无效的手机号码" });
}
var encodedPhone = new Hashids("codewf").EncodeLong(phoneNumberLong);
var generatedUrl = $"{siteOption.Value.Domain}/nuoche?p={encodedPhone}";
var fileName = $"qrcode_{Guid.NewGuid():N}.png";
var qrCodePath = Path.Combine(env.WebRootPath, IconFolder, fileName);
Directory.CreateDirectory(Path.Combine(env.WebRootPath, IconFolder));
QrCodeGenerator.GenerateQrCode(request.Title, generatedUrl, qrCodePath, request.SubTitle);
var qrCodeUrl = $"/{IconFolder}/{fileName}";
return Ok(new
{
success = true,
qrCodeUrl,
generatedUrl
});
}
catch (Exception ex)
{
return BadRequest(new { success = false, message = ex.Message });
}
}
五、功能對比與應用場景
離線桌面版和在線網頁版各有優勢,下面是它們的特點對比:
| 功能 | 離線版 | 在線版 |
|---|---|---|
| 安裝需求 | 需要下載安裝 | 無需安裝,瀏覽器訪問 |
| 平台支持 | Windows、macOS、Linux | 任何現代瀏覽器 |
| 網絡依賴 | 僅初次生成url需要網絡 | 完全依賴網絡 |
| 文件存儲 | 本地永久保存 | 伺服器臨時保存 |
| ui體驗 | 桌面應用風格 | 響應式網頁設計 |
| 拖拽支持 | 支持拖拽二維碼 | 不支持直接拖拽 |
| 隱私保護 | 高(本地處理) | 中(伺服器處理) |
| 小標題功能 | 支持自定義小標題 | 支持自定義小標題 |
小標題功能的加入使二維碼更加信息豐富,可以在二維碼下方添加額外提示信息,如"掃碼聯繫車主"或顯示部分聯繫方式,增強了二維碼的可識別性和使用便捷性。
六、總結與展望
通過本文,我們詳細居間了挪車二維碼生成工具的開發過程,包括需求分析、核心代碼實現、ui設計和多平台部署。這個工具不僅解決了傳統挪車聯繫方式帶來的隱私泄露和號碼濫用問題,還通過現代技術提供了更安全、便捷的挪車體驗。
這個工具在以下場景中特別有用:
- 臨時停車:在小區、商場等地臨時停車時提供聯繫方式
- 車展活動:展示車輛時放置便於聯繫的二維碼
- 共享車位:在私人車位上提供臨時聯繫方式
- 緊急情況:車輛因特殊情況需要緊急聯繫車主時
未來,我們計劃進一步完善這個工具,可能的改進方向包括:
- 多語言支持:添加英語、日語等多語言界面,方便國際用戶使用
- 更多自定義選項:支持自定義二維碼顏色、樣式和布局
- 統計功能:添加掃碼次數統計,幫助用戶了解二維碼使用情況
- 企業版功能:為車隊管理、停車場等企業用戶提供批量生成功能
作為一個實用工具,挪車二維碼生成器解決了現實生活中的實際問題,同時也展示了如何使用現代.net技術棧構建跨平台應用。
希望本文對你有所幫助,如有問題歡迎在評論區留言討論!
源碼參考:


