从Cloudmersive条形码API迁移到IronBarcode
这是.NET条形码历史上最简单的迁移。 移除HTTP客户端。 添加NuGet包。 删除 API 密钥管理。 其余部分只需查找并替换即可。
Cloudmersive的.NET SDK是一个生成的REST API客户端——一个围绕HttpClient调用的薄包装。 应用程序中的每个 Cloudmersive 特定代码要么是该客户端的配置,要么是通过该客户端进行的调用,要么是网络请求失败时发生的错误处理。 IronBarcode不需要任何基础设施,它可以在本地处理条形码,无需网络依赖。
快速入门:三个步骤
第一步:交换包装
dotnet remove package Cloudmersive.APIClient.NET.Barcode
dotnet add package BarCode
步骤 2:替换命名空间
// Remove these
using Cloudmersive.APIClient.NET.Barcode.Api;
using Cloudmersive.APIClient.NET.Barcode.Client;
// Add this
using IronBarCode;Imports IronBarCode步骤 3:将 API 密钥设置替换为许可证密钥
// Remove this
Configuration.Default.ApiKey.Add("Apikey", "YOUR-CLOUDMERSIVE-API-KEY");
// Replace with this — set once at application startup
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";' Remove this
Configuration.Default.ApiKey.Add("Apikey", "YOUR-CLOUDMERSIVE-API-KEY")
' Replace with this — set once at application startup
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"在ASP.NET Core应用程序中,许可证密钥放在builder.Build()之前:
using IronBarCode;
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
var builder = WebApplication.CreateBuilder(args);
// ... rest of startupImports IronBarCode
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"
Dim builder = WebApplication.CreateBuilder(args)
' ... rest of startup成本:你过去的花费 vs 你未来的花费
迁移之前,最好先计算一下 Cloudmersive 集成的实际成本。 计算方法很简单:
| 每日交易量 | 每月请求 | Cloudmersive 层级 | IronBarcode (一次性) |
|---|---|---|---|
| 每天100 | 约3000 | 基础 $19.99/月 | $999 |
| 每天1000人 | 约30,000 | 商务高级 $99.99/月 | $999 |
| 每天 5,000 | 约15万 | 中型企业 $499.99/月 | $999 |
| 每天 10,000 | 约30万 | Enterprise(定制) | $999 |
| 每天 25,000 | 约75万 | Enterprise(定制) | 1499 美元(Plus3 位开发者) |
如果您为Cloudmersive支付的费用超过每月约$63,IronBarcode的Lite许可证将在第一年内自动偿还其费用。 达到盈亏平衡后,IronBarcode是永久许可证,无需续订,也没有每次请求的费用。
代码迁移示例
二维码生成
最常见的 Cloudmersive 生成调用直接映射到IronBarcode:
之前(Cloudmersive):
using Cloudmersive.APIClient.NET.Barcode.Api;
using Cloudmersive.APIClient.NET.Barcode.Client;
Configuration.Default.ApiKey.Add("Apikey", "YOUR-CLOUDMERSIVE-API-KEY");
var apiInstance = new GenerateBarcodeApi();
byte[] result = apiInstance.GenerateBarcodeQRCode("https://example.com");
File.WriteAllBytes("qr.png", result);Imports Cloudmersive.APIClient.NET.Barcode.Api
Imports Cloudmersive.APIClient.NET.Barcode.Client
Configuration.Default.ApiKey.Add("Apikey", "YOUR-CLOUDMERSIVE-API-KEY")
Dim apiInstance As New GenerateBarcodeApi()
Dim result As Byte() = apiInstance.GenerateBarcodeQRCode("https://example.com")
File.WriteAllBytes("qr.png", result)(IronBarcode之后):
using IronBarCode;
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
byte[] result = QRCodeWriter.CreateQrCode("https://example.com", 500)
.ToPngBinaryData();
File.WriteAllBytes("qr.png", result);
// Or save directly
QRCodeWriter.CreateQrCode("https://example.com", 500)
.SaveAsPng("qr.png");Imports IronBarCode
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"
Dim result As Byte() = QRCodeWriter.CreateQrCode("https://example.com", 500).ToPngBinaryData()
File.WriteAllBytes("qr.png", result)
' Or save directly
QRCodeWriter.CreateQrCode("https://example.com", 500).SaveAsPng("qr.png")Code128生成
之前(Cloudmersive):
using Cloudmersive.APIClient.NET.Barcode.Api;
using Cloudmersive.APIClient.NET.Barcode.Client;
Configuration.Default.ApiKey.Add("Apikey", "YOUR-CLOUDMERSIVE-API-KEY");
var apiInstance = new GenerateBarcodeApi();
byte[] result = apiInstance.GenerateBarcodeCode128("SHIP-2024031500428");
File.WriteAllBytes("barcode.png", result);Imports Cloudmersive.APIClient.NET.Barcode.Api
Imports Cloudmersive.APIClient.NET.Barcode.Client
Configuration.Default.ApiKey.Add("Apikey", "YOUR-CLOUDMERSIVE-API-KEY")
Dim apiInstance As New GenerateBarcodeApi()
Dim result As Byte() = apiInstance.GenerateBarcodeCode128("SHIP-2024031500428")
File.WriteAllBytes("barcode.png", result)(IronBarcode之后):
using IronBarCode;
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
BarcodeWriter.CreateBarcode("SHIP-2024031500428", BarcodeEncoding.Code128)
.SaveAsPng("barcode.png");
// Or get bytes directly
byte[] result = BarcodeWriter.CreateBarcode("SHIP-2024031500428", BarcodeEncoding.Code128)
.ToPngBinaryData();Imports IronBarCode
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"
BarcodeWriter.CreateBarcode("SHIP-2024031500428", BarcodeEncoding.Code128) _
.SaveAsPng("barcode.png")
' Or get bytes directly
Dim result As Byte() = BarcodeWriter.CreateBarcode("SHIP-2024031500428", BarcodeEncoding.Code128) _
.ToPngBinaryData()条形码读取
之前(Cloudmersive):
using Cloudmersive.APIClient.NET.Barcode.Api;
using Cloudmersive.APIClient.NET.Barcode.Client;
Configuration.Default.ApiKey.Add("Apikey", "YOUR-CLOUDMERSIVE-API-KEY");
var scanApi = new BarcodeScanApi();
using (var stream = File.OpenRead("barcode.png"))
{
var result = scanApi.BarcodeScanImage(stream);
if (result.Successful == true)
{
Console.WriteLine($"Value: {result.RawText}");
Console.WriteLine($"Type: {result.BarcodeType}");
}
}Imports Cloudmersive.APIClient.NET.Barcode.Api
Imports Cloudmersive.APIClient.NET.Barcode.Client
Configuration.Default.ApiKey.Add("Apikey", "YOUR-CLOUDMERSIVE-API-KEY")
Dim scanApi As New BarcodeScanApi()
Using stream = File.OpenRead("barcode.png")
Dim result = scanApi.BarcodeScanImage(stream)
If result.Successful = True Then
Console.WriteLine($"Value: {result.RawText}")
Console.WriteLine($"Type: {result.BarcodeType}")
End If
End Using(IronBarcode之后):
using IronBarCode;
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
var results = BarcodeReader.Read("barcode.png");
var result = results.First();
Console.WriteLine($"Value: {result.Value}");
Console.WriteLine($"Type: {result.Format}");Imports IronBarCode
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"
Dim results = BarcodeReader.Read("barcode.png")
Dim result = results.First()
Console.WriteLine($"Value: {result.Value}")
Console.WriteLine($"Type: {result.Format}")异步条形码读取
如果您的 Cloudmersive 集成使用异步 API:
之前(Cloudmersive):
using Cloudmersive.APIClient.NET.Barcode.Api;
using Cloudmersive.APIClient.NET.Barcode.Client;
Configuration.Default.ApiKey.Add("Apikey", "YOUR-CLOUDMERSIVE-API-KEY");
var scanApi = new BarcodeScanApi();
using (var stream = File.OpenRead("barcode.png"))
{
var result = await scanApi.BarcodeScanImageAsync(stream);
return result.Successful == true ? result.RawText : null;
}Imports Cloudmersive.APIClient.NET.Barcode.Api
Imports Cloudmersive.APIClient.NET.Barcode.Client
Configuration.Default.ApiKey.Add("Apikey", "YOUR-CLOUDMERSIVE-API-KEY")
Dim scanApi As New BarcodeScanApi()
Using stream = File.OpenRead("barcode.png")
Dim result = Await scanApi.BarcodeScanImageAsync(stream)
Return If(result.Successful = True, result.RawText, Nothing)
End Using(IronBarcode之后):
using IronBarCode;
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
// IronBarcode is synchronous; wrap in Task.Run to free the calling thread for CPU-bound work
var results = await Task.Run(() => BarcodeReader.Read("barcode.png"));
return results.FirstOrDefault()?.Value;Imports IronBarCode
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"
' IronBarcode is synchronous; wrap in Task.Run to free the calling thread for CPU-bound work
Dim results = Await Task.Run(Function() BarcodeReader.Read("barcode.png"))
Return results.FirstOrDefault()?.Value从 PDF 中读取条形码
Cloudmersive本身不支持PDF格式。 如果您的现有代码在扫描前会从 PDF 中提取图像,请将整个流程替换为单个IronBarcode调用:
之前(Cloudmersive——需要单独提取PDF文件):
// Requires a separate PDF library to extract pages as images,
// then one Cloudmersive API call per extracted page image.
var scanApi = new BarcodeScanApi();
foreach (var pageImagePath in ExtractPdfPages("document.pdf"))
{
using var stream = File.OpenRead(pageImagePath);
var result = await scanApi.BarcodeScanImageAsync(stream);
if (result.Successful == true)
{
Console.WriteLine($"Found: {result.RawText}");
}
}Imports System.IO
Imports Cloudmersive.APIClient.NET.BarcodeScan
' Requires a separate PDF library to extract pages as images,
' then one Cloudmersive API call per extracted page image.
Dim scanApi As New BarcodeScanApi()
For Each pageImagePath In ExtractPdfPages("document.pdf")
Using stream As FileStream = File.OpenRead(pageImagePath)
Dim result = Await scanApi.BarcodeScanImageAsync(stream)
If result.Successful = True Then
Console.WriteLine($"Found: {result.RawText}")
End If
End Using
Next(IronBarcode之后):
using IronBarCode;
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
// One call — all pages, all barcodes, no image extraction step
var results = BarcodeReader.Read("document.pdf");
foreach (var result in results)
{
Console.WriteLine($"Page {result.PageNumber}: {result.Value}");
}Imports IronBarCode
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"
' One call — all pages, all barcodes, no image extraction step
Dim results = BarcodeReader.Read("document.pdf")
For Each result In results
Console.WriteLine($"Page {result.PageNumber}: {result.Value}")
Next多条形码检测
如果您有可以扫描带有多个条形码的文档的代码:
之前(Cloudmersive——需要多次调用):
// Cloudmersive returns one barcode per scan — multiple calls for multiple barcodes
var scanApi = new BarcodeScanApi();
var barcodeValues = new List<string>();
foreach (var croppedRegion in CropBarcodeRegions("invoice.png"))
{
using var stream = new MemoryStream(croppedRegion);
var result = await scanApi.BarcodeScanImageAsync(stream);
if (result.Successful == true)
barcodeValues.Add(result.RawText);
}Imports System.IO
Imports System.Collections.Generic
Imports System.Threading.Tasks
' Cloudmersive returns one barcode per scan — multiple calls for multiple barcodes
Dim scanApi As New BarcodeScanApi()
Dim barcodeValues As New List(Of String)()
For Each croppedRegion In CropBarcodeRegions("invoice.png")
Using stream As New MemoryStream(croppedRegion)
Dim result = Await scanApi.BarcodeScanImageAsync(stream)
If result.Successful = True Then
barcodeValues.Add(result.RawText)
End If
End Using
Next(IronBarcode之后):
using IronBarCode;
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
var options = new BarcodeReaderOptions
{
Speed = ReadingSpeed.Balanced,
ExpectMultipleBarcodes = true,
};
// One call — returns all barcodes in the image
var results = BarcodeReader.Read("invoice.png", options);
var barcodeValues = results.Select(r => r.Value).ToList();Imports IronBarCode
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"
Dim options As New BarcodeReaderOptions With {
.Speed = ReadingSpeed.Balanced,
.ExpectMultipleBarcodes = True
}
' One call — returns all barcodes in the image
Dim results = BarcodeReader.Read("invoice.png", options)
Dim barcodeValues = results.Select(Function(r) r.Value).ToList()拆除不再需要的基础设施
在迁移过程中,这一部分需要删除的代码比编写的代码还要多。
移除 HTTP 异常处理
Cloudmersive 操作可能会因网络错误、HTTP 4xx/5xx 响应和超时异常而失败。 这通常会产生IronBarcode不需要的 try/catch 结构:
// Remove all of this
try
{
var result = await scanApi.BarcodeScanImageAsync(stream);
// ...
}
catch (ApiException ex) when (ex.ErrorCode == 429)
{
// Rate limited — wait and retry
await Task.Delay(TimeSpan.FromSeconds(2));
// retry logic...
}
catch (ApiException ex) when (ex.ErrorCode == 503)
{
// Service unavailable — Cloudmersive is down
_logger.LogError("Cloudmersive unavailable: {Message}", ex.Message);
throw;
}
catch (HttpRequestException ex)
{
// Network error
_logger.LogError("Network failure: {Message}", ex.Message);
throw;
}
catch (TaskCanceledException)
{
// Timeout
_logger.LogError("Cloudmersive request timed out");
throw;
}Imports System
Imports System.Threading.Tasks
' Remove all of this
Try
Dim result = Await scanApi.BarcodeScanImageAsync(stream)
' ...
Catch ex As ApiException When ex.ErrorCode = 429
' Rate limited — wait and retry
Await Task.Delay(TimeSpan.FromSeconds(2))
' retry logic...
Catch ex As ApiException When ex.ErrorCode = 503
' Service unavailable — Cloudmersive is down
_logger.LogError("Cloudmersive unavailable: {Message}", ex.Message)
Throw
Catch ex As HttpRequestException
' Network error
_logger.LogError("Network failure: {Message}", ex.Message)
Throw
Catch ex As TaskCanceledException
' Timeout
_logger.LogError("Cloudmersive request timed out")
Throw
End TryIronBarcode不会抛出网络异常。 如果无法从输入中解码条形码,它会抛出BarcodeException,这是内容问题而非基础设施问题。 简化的错误处理:
using IronBarCode;
var results = BarcodeReader.Read("barcode.png");
if (!results.Any())
{
_logger.LogWarning("No barcode detected in image");
}Imports IronBarCode
Dim results = BarcodeReader.Read("barcode.png")
If Not results.Any() Then
_logger.LogWarning("No barcode detected in image")
End If移除重试逻辑
如果您的 Cloudmersive 集成包含重试策略(例如 Polly、自定义重试循环或指数退避),请将其完全移除:
// Remove retry infrastructure
// var retryPolicy = Policy
// .Handle<ApiException>()
// .WaitAndRetryAsync(3, retryAttempt =>
// TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
// IronBarcode — no retry needed
var results = BarcodeReader.Read("barcode.png");' Remove retry infrastructure
' Dim retryPolicy = Policy _
' .Handle(Of ApiException)() _
' .WaitAndRetryAsync(3, Function(retryAttempt) _
' TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))
' IronBarcode — no retry needed
Dim results = BarcodeReader.Read("barcode.png")移除速率限制跟踪
如果您的代码跟踪 API 调用次数以保持在 Cloudmersive 的每月配额范围内:
// Remove quota tracking
// private static int _monthlyCallCount = 0;
// private const int MonthlyLimit = 30000;
//
// if (Interlocked.Increment(ref _monthlyCallCount) > MonthlyLimit)
// throw new InvalidOperationException("Monthly Cloudmersive quota exceeded");
// IronBarcode — no quota
var results = BarcodeReader.Read("barcode.png");' Remove quota tracking
' Private Shared _monthlyCallCount As Integer = 0
' Private Const MonthlyLimit As Integer = 30000
'
' If Interlocked.Increment(_monthlyCallCount) > MonthlyLimit Then
' Throw New InvalidOperationException("Monthly Cloudmersive quota exceeded")
' IronBarcode — no quota
Dim results = BarcodeReader.Read("barcode.png")移除网络超时配置
Cloudmersive客户端通常包含超时配置。 移除它:
// Remove timeout configuration
// Configuration.Default.Timeout = 30000; // 30 second timeout
// Configuration.Default.ConnectionTimeout = 5000;
// IronBarcode has no network timeout — no network' Remove timeout configuration
' Configuration.Default.Timeout = 30000 ' 30 second timeout
' Configuration.Default.ConnectionTimeout = 5000
' IronBarcode has no network timeout — no network移除 API 密钥轮换
如果您的应用程序按计划轮换 API 密钥或将其存储在密钥管理系统中:
// Simplify this pattern
// private string GetCloudmersiveApiKey() =>
// _secretManager.GetSecret("cloudmersive-api-key-current");
//
// Configuration.Default.ApiKey["Apikey"] = GetCloudmersiveApiKey();
// IronBarcode — set once at startup
IronBarCode.License.LicenseKey = _configuration["IronBarcode:LicenseKey"];
// or literal: "YOUR-LICENSE-KEY"' Simplify this pattern
' Private Function GetCloudmersiveApiKey() As String
' Return _secretManager.GetSecret("cloudmersive-api-key-current")
' End Function
'
' Configuration.Default.ApiKey("Apikey") = GetCloudmersiveApiKey()
' IronBarcode — set once at startup
IronBarCode.License.LicenseKey = _configuration("IronBarcode:LicenseKey")
' or literal: "YOUR-LICENSE-KEY"常见迁移问题
Cloudmersive返回的byte[]用于生成
Cloudmersive的生成方法直接返回byte[]。 IronBarcode返回一个GeneratedBarcode对象,为您提供更多选项:
// Cloudmersive returned byte[] directly
byte[] cloudmersiveResult = apiInstance.GenerateBarcodeQRCode("data");
// IronBarcode — call ToPngBinaryData() for the equivalent byte[]
byte[] ironBarcodeResult = BarcodeWriter.CreateBarcode("data", BarcodeEncoding.QRCode)
.ToPngBinaryData();
// Or save directly (often simpler)
BarcodeWriter.CreateBarcode("data", BarcodeEncoding.QRCode)
.SaveAsPng("output.png");
// Or get other formats
byte[] jpegBytes = BarcodeWriter.CreateBarcode("data", BarcodeEncoding.QRCode)
.ToJpegBinaryData();Imports Cloudmersive.APIClient
Imports IronBarCode
' Cloudmersive returned byte() directly
Dim cloudmersiveResult As Byte() = apiInstance.GenerateBarcodeQRCode("data")
' IronBarcode — call ToPngBinaryData() for the equivalent byte()
Dim ironBarcodeResult As Byte() = BarcodeWriter.CreateBarcode("data", BarcodeEncoding.QRCode) _
.ToPngBinaryData()
' Or save directly (often simpler)
BarcodeWriter.CreateBarcode("data", BarcodeEncoding.QRCode) _
.SaveAsPng("output.png")
' Or get other formats
Dim jpegBytes As Byte() = BarcodeWriter.CreateBarcode("data", BarcodeEncoding.QRCode) _
.ToJpegBinaryData()Cloudmersive 阅读返回了 JSON 响应模型
Cloudmersive的扫描结果是一个响应模型,具有诸如BarcodeType这样的属性。 IronBarcode返回类型化的结果对象:
// Cloudmersive result model
if (cloudmersiveResult.Successful == true)
{
string value = cloudmersiveResult.RawText;
string type = cloudmersiveResult.BarcodeType;
}
// IronBarcode result — use .Value and .Format directly
var results = BarcodeReader.Read("barcode.png");
if (results.Any())
{
string value = results.First().Value;
string format = results.First().Format.ToString();
}' Cloudmersive result model
If cloudmersiveResult.Successful = True Then
Dim value As String = cloudmersiveResult.RawText
Dim type As String = cloudmersiveResult.BarcodeType
End If
' IronBarcode result — use .Value and .Format directly
Dim results = BarcodeReader.Read("barcode.png")
If results.Any() Then
Dim value As String = results.First().Value
Dim format As String = results.First().Format.ToString()
End If多种 Cloudmersive API 实例类型
Cloudmersive将生成和扫描分隔为BarcodeScanApi实例。 IronBarcode使用静态类,无需实例:
// Cloudmersive — two separate API instances
var generateApi = new GenerateBarcodeApi();
var scanApi = new BarcodeScanApi();
// IronBarcode — static, no instances
// BarcodeWriter for generation
// BarcodeReader for reading
// QRCodeWriter for QR codes specifically' Cloudmersive — two separate API instances
Dim generateApi As New GenerateBarcodeApi()
Dim scanApi As New BarcodeScanApi()
' IronBarcode — static, no instances
' BarcodeWriter for generation
' BarcodeReader for reading
' QRCodeWriter for QR codes specifically迁移清单
在迁移前后,请在代码库中运行以下搜索,以验证是否已捕获所有 Cloudmersive 引用:
# Find all Cloudmersive references
grep -r "Cloudmersive" --include="*.cs" .
grep -r "GenerateBarcodeApi" --include="*.cs" .
grep -r "BarcodeScanApi" --include="*.cs" .
grep -r "Configuration.Default.ApiKey" --include="*.cs" .
grep -r '"Apikey"' --include="*.cs" .
grep -r "GenerateBarcodeQRCode" --include="*.cs" .
grep -r "GenerateBarcodeCode128" --include="*.cs" .
grep -r "BarcodeScanImage" --include="*.cs" .
grep -r "cloudmersive" --include="*.csproj" .
grep -r "cloudmersive" --include="*.json" .
迁移后,所有搜索结果应为零。 然后验证IronBarcode是否已正确引用:
grep -r "IronBarCode" --include="*.cs" .
grep -r "BarcodeReader" --include="*.cs" .
grep -r "BarcodeWriter" --include="*.cs" .
配置清单
- 从所有
Cloudmersive.APIClient.NET.Barcode - 从
appsettings.json、环境变量和秘密存储中删除Cloudmersive API密钥 - 添加IronBarcode NuGet包
- 在应用程序启动时添加
IronBarCode.License.LicenseKey初始化 - 移除 Cloudmersive 调用的重试策略和熔断器
- 移除速率限制跟踪代码
- 移除网络超时配置
测试清单
迁移完成后:
- 验证二维码生成器是否产生有效、可扫描的输出
- 验证 Code128 和其他线性条码的生成
- 验证常见图像格式(PNG、JPEG、BMP)中的条形码读取情况
- 如果您要处理 PDF 文档,请测试 PDF 读取功能
- 如有需要,测试多条形码检测
- 确认网络流量中没有出现 Cloudmersive API 调用
- 确认应用程序在没有网络连接的情况下也能正常运行
迁移后你能获得什么
除了节省成本之外,迁移还消除了一整类运营方面的顾虑:
**不再依赖网络进行条形码处理。**无论 Cloudmersive 是否可用、您的网络连接质量如何,或者网络延迟出现峰值,您的文档处理流程都能正常运行。
**不再需要进行配额管理。**月底文档量激增不会导致意外超支或处理失败。
**数据不再离开您的网络。**包含敏感客户信息、财务信息或健康信息的条形码图像将保留在您的基础设施中。
**代码更简洁。**生产环境 Cloudmersive 集成所需的重试机制、超时处理和 HTTP 异常管理都已移除。 取而代之的是直接方法调用。
这种迁移是机械性的。 这样做的好处在于结构上——消除了工作流程中对外部网络服务的依赖,而该工作流程原本并不需要外部网络服务。

Curtis Chau 拥有卡尔顿大学的计算机科学学士学位,专注于前端开发,精通 Node.js、TypeScript、JavaScript 和 React。他热衷于打造直观且美观的用户界面,喜欢使用现代框架并创建结构良好、视觉吸引力强的手册。