PAYATHON 2026

如何在 C# Windows 应用中启用 POS 支付并读取银行卡数据?

支付小周

明确结论

KeyPress 不是 POS 支付的通用接口。刷卡后会不会触发 KeyPress,取决于读卡设备是否处于 USB HID Keyboard(键盘模拟、Keyboard Wedge)模式:

  • 设备模拟键盘时,会把磁条内容转换成一连串“按键”,Windows 随后产生相应的键盘事件。
  • 设备通过串口、虚拟 COM、厂商驱动或专用 SDK 工作时,不会自动触发 KeyPress。应用需要通过对应的接口读取数据。
  • EMV 芯片卡、非接触式银行卡和 PIN Pad 通常要使用支付终端 SDK 或支付网关提供的集成方案,不能依靠 KeyPress 完成交易。

在正式支付系统中,也不应把完整卡号或磁道数据直接发送到 supervisor application。更合适的做法是使用支付服务商提供的 半集成终端、端到端加密和令牌化方案。业务应用只接收交易结果、token、卡品牌和掩码卡号,例如 ************1234。

为什么刷卡会触发 KeyPress

在 Windows 看来,键盘模拟型磁条读卡器就是一只普通键盘。刷卡时,设备可能依次输入:

%...?...;
...?

输入结束后,设备还会发送 Enter、Tab 或其他结束字符。由于这些内容以键盘扫描码的形式输入,当前拥有焦点的窗口会收到 KeyDown、KeyPress 和 KeyUp 等事件。

这不是支付网关或驱动主动调用了应用的 KeyPress() 方法,而是 Windows 将设备输入识别为键盘输入,再分发给窗口。

这种方式通常要满足以下条件:

  1. 读卡器支持并已启用 HID Keyboard 模式。
  2. Windows 已正确识别该设备。
  3. POS 窗口或输入控件拥有键盘焦点。
  4. 读卡器配置了应用可以识别的结束符,例如 Enter。
  5. 应用订阅了正确窗口或控件的键盘事件。

设备模式、前缀、后缀和结束符一般通过厂商配置工具设置。不同型号提供的选项可能不同,需要查阅对应设备的手册。

先确定设备属于哪一种类型

键盘模拟型读卡器

可以先打开记事本,再刷一张测试卡。如果记事本中立刻出现一串字符,设备很可能正在使用键盘模拟模式。

这种模式容易接入,但限制也很明显:

  • 数据会被输入到当前拥有焦点的窗口。
  • 用户的键盘输入可能和刷卡数据混在一起。
  • 其他程序也可能收到这些数据。
  • 应用很难可靠地区分实体键盘和读卡器。
  • 原始磁道数据会进入应用进程,带来较高的合规和安全风险。

串口或虚拟 COM 设备

如果设备管理器中出现 COM3、COM4 等端口,应用通常需要使用 System.IO.Ports.SerialPort,并按照厂商协议设置波特率、数据位、校验位和停止位。

厂商 SDK 或支付终端

EMV、NFC、PIN 输入和加密读卡设备通常通过厂商 SDK、OPOS、JavaPOS、Windows 服务、HTTP 本地代理或支付处理商 API 通信。

应用从这类设备收到的可能是加密块、token 或交易结果,而不是可以直接显示的卡号字符串。具体如何实现,应以终端厂商和收单机构提供的接口文档为准。

WinForms 中接收键盘模拟输入

下面的示例会缓冲设备模拟的按键,并在收到 Enter 后处理一条完整输入。它只适用于 HID Keyboard 模式,不是完整的支付实现。

using System;
using System.Text;
using System.Windows.Forms;

public partial class PosForm : Form
{
    private readonly StringBuilder _readerBuffer = new();

    public PosForm()
    {
        InitializeComponent();

        // 让窗体在子控件之前接收键盘事件。
        KeyPreview = true;
        KeyPress += PosForm_KeyPress;
    }

    private void PosForm_KeyPress(object? sender, KeyPressEventArgs e)
    {
        if (e.KeyChar == '\r')
        {
            string deviceInput = _readerBuffer.ToString();
            _readerBuffer.Clear();

            e.Handled = true;

            if (deviceInput.Length > 0)
            {
                ProcessReaderInput(deviceInput);
            }

            return;
        }

        if (!char.IsControl(e.KeyChar))
        {
            _readerBuffer.Append(e.KeyChar);
            e.Handled = true;
        }
    }

    private void ProcessReaderInput(string deviceInput)
    {
        try
        {
            // 不要写入日志、剪贴板或普通数据库。
            // 正式系统应把数据交给经过认证的支付组件处理。
            paymentStatusLabel.Text = "已收到读卡设备输入";
        }
        finally
        {
            // string 在 .NET 中不可变,无法保证从内存中立即擦除。
            // 因此应尽量避免让原始卡数据进入业务应用。
            deviceInput = string.Empty;
        }
    }
}

这个示例仍有一个实际限制:KeyPreview 只能接收发送到当前窗体的键盘消息。如果应用不在前台、焦点落在其他程序中,或者某些控件截获了按键,事件就可能无法按预期触发。

也不能只凭“字符输入速度很快”判断输入来自读卡器。如果需要可靠地区分多个 HID 设备,可以考虑使用 Windows Raw Input API,根据设备句柄识别输入来源,也可以直接使用厂商 SDK。

读取串口设备的示例

如果读卡器提供虚拟 COM 端口,可以使用 SerialPort。端口参数必须与设备配置一致,示例中的值不能直接照搬。

using System;
using System.IO.Ports;
using System.Text;
using System.Windows.Forms;

public partial class PosForm : Form
{
    private readonly StringBuilder _serialBuffer = new();
    private SerialPort? _cardReaderPort;

    public PosForm()
    {
        InitializeComponent();
    }

    private void OpenCardReader()
    {
        _cardReaderPort = new SerialPort
        {
            PortName = "COM3",
            BaudRate = 9600,
            DataBits = 8,
            Parity = Parity.None,
            StopBits = StopBits.One,
            Handshake = Handshake.None,
            Encoding = Encoding.ASCII,
            NewLine = "\r"
        };

        _cardReaderPort.DataReceived += CardReaderPort_DataReceived;
        _cardReaderPort.Open();
    }

    private void CardReaderPort_DataReceived(
        object sender,
        SerialDataReceivedEventArgs e)
    {
        if (_cardReaderPort is null)
        {
            return;
        }

        string chunk = _cardReaderPort.ReadExisting();

        lock (_serialBuffer)
        {
            _serialBuffer.Append(chunk);

            int terminatorIndex;
            while ((terminatorIndex =
                    _serialBuffer.ToString().IndexOf('\r')) >= 0)
            {
                string message = _serialBuffer
                    .ToString(0, terminatorIndex);

                _serialBuffer.Remove(0, terminatorIndex + 1);

                BeginInvoke(new Action(() =>
                {
                    ProcessSerialMessage(message);
                }));
            }
        }
    }

    private void ProcessSerialMessage(string message)
    {
        paymentStatusLabel.Text = "已收到串口设备数据";

        // 不要在这里显示或记录完整磁道数据。
        // 应按照设备厂商协议将数据提交给安全支付组件。
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        if (_cardReaderPort is not null)
        {
            _cardReaderPort.DataReceived -= CardReaderPort_DataReceived;

            if (_cardReaderPort.IsOpen)
            {
                _cardReaderPort.Close();
            }

            _cardReaderPort.Dispose();
        }

        base.OnFormClosed(e);
    }
}

DataReceived 在后台线程中执行,所以更新 WinForms 控件时,需要通过 BeginInvoke 切回 UI 线程。实际协议还可能采用 STX/ETX、校验和或二进制数据,不能简单假定每条消息都以回车结束。

正式 POS 支付的推荐流程

完整的支付实现通常不是先读取字符串,再调用支付网关。常见流程如下:

  1. 选择支付处理商及其支持的终端型号。
  2. 由支付处理商或收单机构完成商户和终端配置。
  3. 安装终端驱动、服务或 SDK。
  4. POS 应用通过 SDK 发起交易,并传入金额、币种和订单号。
  5. 顾客在支付终端上插卡、挥卡或刷卡。
  6. 终端完成读卡、PIN、加密和必要的持卡人验证。
  7. 终端或支付服务与收单系统通信。
  8. POS 应用接收批准、拒绝、取消或超时结果。
  9. 应用保存交易 ID、授权码、token 和掩码卡号,不保存磁道数据。

伪代码通常类似:

var request = new PaymentRequest
{
    Amount = 125.50m,
    Currency = "CNY",
    OrderId = "ORDER-20260901-001"
};

PaymentResult result =
    await paymentTerminal.StartSaleAsync(request);

if (result.Approved)
{
    SavePaymentResult(
        result.TransactionId,
        result.AuthorizationCode,
        result.CardBrand,
        result.MaskedPan,
        result.PaymentToken);
}
else
{
    ShowPaymentFailure(result.ResponseMessage);
}

PaymentRequest、PaymentResult 和 StartSaleAsync 只是用来说明集成结构的占位名称,并不是通用的 .NET API。实际使用的名称由支付终端 SDK 决定。

关于卡号和持卡人姓名

并不是所有银行卡或交易方式都会提供持卡人姓名:

  • 磁条 Track 1 可能包含姓名,但该字段不一定存在,也不一定可靠。
  • Track 2 通常包含主账号、有效期和服务代码等数据,不包含姓名。
  • EMV 芯片交易能否返回姓名,取决于卡片数据、终端配置和 SDK。
  • 非接触式钱包交易可能只返回与网络 token 对应的信息。
  • 出于安全或合规要求,支付处理商可能只返回掩码卡号。

因此,不能把“读取持卡人姓名”设为支付成功的必要条件,也不应使用卡片上的姓名完成身份认证。

安全与合规注意事项

原始磁道数据是高度敏感的支付数据。生产环境至少要遵守以下原则:

  • 不记录完整 PAN、Track 1、Track 2、PIN、PIN block 或验证码。
  • 不把原始卡数据写入日志、异常信息、剪贴板、消息队列或分析系统。
  • 不使用普通 HTTP、共享文件或自定义明文协议传输卡数据。
  • 不在 supervisor 屏幕上显示完整卡号,只显示允许的掩码形式。
  • 不保存刷卡后的完整磁道数据,即使交易失败也不能保存。
  • 优先选择支持点到点加密或端到端加密的终端。
  • 使用支付处理商返回的 token 完成退款、撤销或后续业务。
  • 根据实际业务范围评估并满足适用的 PCI DSS 要求。

一旦业务应用直接接触明文磁道数据,合规范围和安全责任都会明显扩大。除非设备厂商和支付处理商明确要求,否则不要自行解析磁道字符串。

KeyPress 只是旧式读卡器的一种输入方式,并不是启用 POS 支付的核心接口。应先确认设备接口和工作模式,再按照厂商 SDK 或支付处理商的认证流程完成集成,这样才能得到可靠并可上线的支付实现。

备注:内容仅供参考。