ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

在正点原子Linux开发板上用QT集成CVR-100UC身份证阅读器

在正点原子Linux开发板上用QT集成CVR-100UC身份证阅读器 做嵌入式QT应用的时候最烦的不是界面写了多少页面而是硬件数据到底怎么才能稳定跑起来。身份证阅读器这种设备更典型Windows下有厂商SDK接口文档写得明明白白鼠标点两下就能读卡到了Linux ARM开发板上驱动找不到、SDK没有、协议资料全靠自己搜很多人第一步就卡死在设备节点上。这篇东西记录的是我在正点原子Linux开发板上集成CVR-100UC身份证阅读器的完整过程。CVR-100UC是一款很常见的USB接口身份证阅读器走标准HID协议Linux内核自带的usbhid驱动就能识别不需要自己编译内核模块也不依赖厂商闭源驱动。我们要做的事情就是打开hidraw设备节点、读取设备上报的原始字节、再按照身份证信息的数据结构把字段拆出来拿到姓名、性别、民族、出生日期、住址、身份证号这些核心信息。界面部分我用QT来写在正点原子IMX6ULL开发板上已经跑通同样的思路放到正点原子RK3568、RK3588这类Linux开发板上也没问题。如果你正在做自助终端、访客登记、门禁系统或者政务窗口设备又恰好要在ARM Linux板子上接身份证阅读器这篇文章应该能帮你少走不少弯路。我会从方案选型、交叉编译环境、完整代码、常见问题几个角度逐步展开最后还会聊几句隐私合规方面很容易被忽略的事情。1. 方案选型与整体思路1.1 CVR-100UC在Linux下为什么能免驱CVR-100UC这类USB身份证阅读器的本质是“通过USB HID协议把身份证芯片里的信息读出来”。身份证本身采用非接触式RF通信阅读器内部集成了射频模块和安全模块上位机不需要关心底层射频怎么做只需要通过USB协议读写数据就行。Windows上厂商把协议封装成了SDK和驱动所以用起来觉得有“官方支持”Linux内核里其实早就实现了usbhid这个通用驱动CVR-100UC插上之后会被内核枚举成一个hidraw字符设备直接像普通文件一样读写就可以通信。这就是为什么不需要去下载什么“Linux版驱动”。设备插入后系统会在/dev目录下生成类似/dev/hidraw0、/dev/hidraw1这样的节点。问题在于板上可能同时插了鼠标、键盘、USB摄像头之类的设备光看名字判断不了哪个节点对应阅读器。比较靠谱的做法是通过/sys目录去确认设备信息后面我会给出一段命令手动验证一次之后再考虑写进程序里自动匹配。1.2 为什么UI方案选QT正点原子Linux开发板一般配LCD屏或者HDMI显示屏可选界面方案其实不少纯C加LVGL、C加GTK、QT甚至拿Web前端套个壳都有人做。我最后选了QT主要是三个原因。第一是开发效率。QT的Widgets模块对表单类界面非常友好一个QTextEdit加几个QLabel就能拼出操作界面不追求花哨的话半天就能搭完。第二是线程模型。身份证读卡属于典型的长连接阻塞式I/O必须放到后台线程去读QT的信号槽机制让后台线程和UI线程之间传数据特别自然不需要写一堆回调函数或者手动管理锁。第三是生态成熟。正点原子官方资料里已经带了能在板子上直接跑的QT运行库社区里有人踩过无数坑遇到问题容易搜到答案这一点在嵌入式开发里其实比技术先进性重要得多。1.3 为什么用hidraw而不是libusb不少人一听到“免驱”就想着上libusb其实没什么必要。我把两种方式对比一下。方式优点缺点hidraw内核已支持open/read/write就能通信代码量少无法处理复杂控制传输部分特殊设备不适用libusb能控制USB原始包适配性强跨平台能力好需要单独移植libusb库要处理endpoint和report细节交叉编译麻烦身份证阅读器这种免驱设备数据交互基本都在中断端点上hidraw已经帮我们包装好了。直接打开节点读取省掉一层第三方库依赖交叉编译的时候也能少踩很多坑。所以我的整个项目里没有任何USB第三方库代码量一下子就压缩下来了。2. 正点原子开发板环境准备与QT交叉编译2.1 用正点原子资料包快速搭好交叉编译工具链写代码之前先把板子的开发环境理清楚。正点原子的资料下载包里有针对各型号开发板的交叉编译工具链比如IMX6ULL对应的arm-linux-gnueabihf工具链。一般的做法是在Ubuntu虚拟机里解压到一个固定目录比如/opt/然后把bin目录加入PATH。检查工具链是否正常直接在终端执行arm-linux-gnueabihf-gcc -v只要能正常打印出版本信息说明工具链可用。这里有个特别容易搞混的点PC自带的gcc编译出来的程序不能在ARM板子上运行所以每一步编译都必须明确使用交叉编译工具链不要图方便直接用系统gcc。2.2 QT库的交叉编译与qmake确认跑QT程序还需要板子上有对应架构的QT运行库。正点原子出厂文件系统一般已经包含可用的QT库开发阶段建议直接用厂家编译好的版本能省掉自己configure一遍的麻烦。如果是从零开始编译QT里面涉及的依赖会非常多tslib触摸库、libpng、jpeg、freetype一个都不能少编译顺序错了要么报错要么显示异常非常折腾。不管用哪种方式最终都要有一个面向ARM架构的qmake。这一点是最容易出问题的。很多人直接在Ubuntu上执行系统自带的qmake编译出来的程序实际上是x86架构拷到板子上当然跑不起来。确认方法很简单先看路径和版本/opt/qt5.9.9/bin/qmake -v如果看到路径在/opt/xxx下并且没有报host system之类异常基本可以认为这个qmake对应的是交叉编译环境。之后在QT工程目录下执行/opt/qt5.9.9/bin/qmake idcard_reader.pro make生成的可执行文件就是ARM版拷贝到板子上直接运行即可。2.3 部署到开发板环境变量与中文字体编译完成后把可执行文件拷贝到开发板。方式有很多NFS、scp、U盘都行。这里最容易踩坑的是运行库路径和QPA平台插件没配置好。板子上运行之前先设置环境变量export QT_QPA_PLATFORMlinuxfb:fb/dev/fb0 export QT_QPA_FONTDIR/usr/share/fonts export LD_LIBRARY_PATH/opt/qt5.9.9/lib:$LD_LIBRARY_PATH ./idcard_reader如果用的是RK3568这类带GPU的板子可以换eglfs平台插件显示效率更高。界面出现中文方块或者乱码的时候多数不是程序逻辑问题而是字体没放对地方。把常见的中文字体文件放到QT_QPA_FONTDIR指定的目录比如wqy-microhei.ttc、simhei.ttf再重启程序看效果。这一步虽然看起来基础但能排除掉一大批“乱码”误判。3. CVR-100UC核心代码实现从hidraw读到UI显示3.1 线程模型为什么不能直接在UI主线程read很多第一次接触这个项目的人会在窗口构造函数里open设备然后直接while循环read。这样会带来两个问题一是read是阻塞的只要设备不返回数据整个UI就会卡死二是程序退出时如果线程正卡在阻塞read上设备资源无法及时释放导致退出不了或者重启后设备被占住。正确做法是把设备读取放进独立线程UI线程收到数据后更新界面两边各干各的互不阻塞。QT支持多线程的方式不少我用了继承QThread重写run()的方案。虽然Qt官方文档更推荐“Worker对象加moveToThread”但对这种简单读卡器场景QThread子类反而更直观。线程内部用select加超时方式轮询可读事件既能快速响应设备数据又能及时响应退出请求这个设计看起来简单实际体验下来非常稳。3.2 打开hidraw设备并监听数据在写代码之前先手动确认设备节点。在开发板终端插入读卡器后执行for f in /sys/class/hidraw/hidraw*/device/uevent; do echo $f ; cat $f; done这个命令会把所有hidraw设备对应的uevent信息打印出来里面有HID_NAME、HID_ID等字段。如果HID_NAME里出现类似CVR或者身份证阅读器相关的名字就说明找到了对应节点记下hidraw编号填进程序里的设备路径。下面是后台读取线程头文件#ifndef IDCARDREADER_H #define IDCARDREADER_H #include QThread #include QByteArray #include QAtomicInt class IdCardReader : public QThread { Q_OBJECT public: explicit IdCardReader(const QString devicePath, QObject *parent nullptr); ~IdCardReader() override; void stop(); signals: void openFailed(const QString reason); void dataReady(const QByteArray data); protected: void run() override; private: QString m_devicePath; QAtomicInt m_stop; }; #endif对应实现文件#include idcardreader.h #include errno.h #include fcntl.h #include string.h #include sys/select.h #include unistd.h IdCardReader::IdCardReader(const QString devicePath, QObject *parent) : QThread(parent) , m_devicePath(devicePath) , m_stop(0) { } IdCardReader::~IdCardReader() { stop(); wait(); } void IdCardReader::stop() { m_stop.store(1); } void IdCardReader::run() { int fd ::open(m_devicePath.toLocal8Bit().constData(), O_RDONLY | O_NONBLOCK); if (fd 0) { emit openFailed(QStringLiteral(无法打开 %1: %2) .arg(m_devicePath) .arg(QString::fromLocal8Bit(strerror(errno)))); return; } char buf[512]; while (!m_stop.load()) { fd_set fds; FD_ZERO(fds); FD_SET(fd, fds); struct timeval timeout; timeout.tv_sec 0; timeout.tv_usec 200 * 1000; int ret select(fd 1, fds, NULL, NULL, timeout); if (ret 0) { continue; } ssize_t n ::read(fd, buf, sizeof(buf)); if (n 0) { emit dataReady(QByteArray(buf, static_castint(n))); } else if (n 0 errno ! EAGAIN) { break; } } ::close(fd); }这段代码里用了O_NONBLOCK加select设备有数据时才继续读没有数据时最多等200毫秒再轮询一次所以stop()之后线程能很快退出不会卡在read上。buffer大小我取了512字节CVR-100UC这类设备上报的身份证数据基本都在这个范围内如果担心某些版本数据更长可以加到1024影响不大。需要说明的是部分设备不是“自动上报”模式必须由上位机先发送一条寻卡命令设备才开始读卡。如果你的设备在插入身份证后上面的代码没有触发dataReady信号多半就是这个原因。解决办法是把厂商协议文档里的命令帧找出来在run()循环里周期调用write发送然后再继续等read。这类命令一般是一串固定字节网上也能搜到不少类似型号的参考具体以你手上设备的实际固件为准。3.3 身份证数据解析字节偏移与GBK转码当dataReady信号带着原始字节传到主线程后下一步是解析。身份证信息在协议里的排列有一定规律常见的标准信息项顺序是姓名15字节、性别1字节、民族2字节、出生日期8字节、住址35字节、身份证号18字节、签发机关30字节、有效期限16字节总计125字节。不同固件可能在这个基础上增加若干字节的包头、状态字或者保留字段所以解析逻辑最好做成可配置。这里我要分享一个非常实用的小技巧解析出来之后先校验身份证号码的校验位。身份证号码第18位是校验码由前17位通过加权计算得到。如果解析出来的身份证号能通过校验说明这组字段偏移是对的如果校验失败就尝试跳过多余的包头字节再解析一次。这个机制可以在一定程度自动适配不同固件版本。先定义信息结构体和解析函数#ifndef IDCARDINFO_H #define IDCARDINFO_H #include QString #include QMetaType struct IdCardInfo { QString name; QString gender; QString nation; QString birth; QString address; QString idNumber; QString authority; QString validity; bool valid false; }; Q_DECLARE_METATYPE(IdCardInfo) #endif#ifndef IDCARDPARSER_H #define IDCARDPARSER_H #include QByteArray #include idcardinfo.h bool parseIdCardData(const QByteArray data, IdCardInfo info); #endif解析实现#include idcardparser.h #include QTextCodec #include QList static QString decodeGBK(const QByteArray raw) { QTextCodec *codec QTextCodec::codecForName(GBK); if (!codec) { return QString::fromLocal8Bit(raw); } QString text codec-toUnicode(raw); while (!text.isEmpty() (text.endsWith(QChar(\0)) || text.endsWith(QChar( )) || text.endsWith(QChar(\n)) || text.endsWith(QChar(\r)))) { text.chop(1); } return text; } static bool verifyIdNumber(const QString id) { if (id.length() ! 18) { return false; } const int weights[17] {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}; const char map[] 10X98765432; int sum 0; for (int i 0; i 17; i) { if (!id.at(i).isDigit()) { return false; } sum id.at(i).digitValue() * weights[i]; } int mod sum % 11; QChar check QLatin1Char(map[mod]); return id.at(17).toUpper() check; } static IdCardInfo parseWithOffset(const QByteArray data, int offset) { IdCardInfo info; const int totalLen 15 1 2 8 35 18 30 16; if (data.size() offset totalLen) { return info; } int pos offset; info.name decodeGBK(data.mid(pos, 15)); pos 15; info.gender (data.at(pos) 1) ? QStringLiteral(男) : QStringLiteral(女); pos 1; info.nation decodeGBK(data.mid(pos, 2)); pos 2; info.birth decodeGBK(data.mid(pos, 8)); pos 8; info.address decodeGBK(data.mid(pos, 35)); pos 35; info.idNumber decodeGBK(data.mid(pos, 18)); pos 18; info.authority decodeGBK(data.mid(pos, 30)); pos 30; info.validity decodeGBK(data.mid(pos, 16)); return info; } bool parseIdCardData(const QByteArray data, IdCardInfo info) { QListint offsetCandidates; if (data.size() 3 (quint8)data.at(0) 0xAA (quint8)data.at(1) 0xAA) { offsetCandidates 3; } offsetCandidates 0; for (int offset : offsetCandidates) { IdCardInfo tmp parseWithOffset(data, offset); if (verifyIdNumber(tmp.idNumber)) { info tmp; info.valid true; return true; } } info parseWithOffset(data, 0); return false; }这里为什么要做GBK转码我多解释一句。身份证信息里的汉字在协议层是按GBK编码传输的而QT里的QString内部是Unicode直接用QString::fromUtf8去转GBK字节一定会变成乱码。decodeGBK函数通过QTextCodec统一转换再把结尾的空字符裁剪掉这是最常规也最稳的做法。身份证号校验算法本身不复杂但加上这个校验之后调试时能省非常多事。程序能自己判断这次解析出来的身份证号是不是合法不用每次都对着十六进制原始数据肉眼猜字段位置这对于偏移不确定的设备来说价值非常大。3.4 主窗口与信号槽对接解析函数准备好之后主窗口的工作就简单了提供设备路径输入框、开始和停止按钮、一个用于显示信息的QTextEdit。点击“开始读取”时创建IdCardReader线程并启动线程发出dataReady信号时调用处理函数线程打开设备失败时通过openFailed信号把错误显示出来。#ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow #include idcardinfo.h #include idcardreader.h class QLineEdit; class QTextEdit; class QPushButton; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); private slots: void startRead(); void stopRead(); void onDataReady(const QByteArray data); void onOpenFailed(const QString reason); private: QLineEdit *m_deviceEdit; QPushButton *m_startBtn; QPushButton *m_stopBtn; QTextEdit *m_infoEdit; IdCardReader *m_reader; }; #endif主窗口实现#include mainwindow.h #include QHBoxLayout #include QVBoxLayout #include QLineEdit #include QPushButton #include QTextEdit #include QWidget #include idcardparser.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , m_deviceEdit(new QLineEdit(QStringLiteral(/dev/hidraw0), this)) , m_startBtn(new QPushButton(QStringLiteral(开始读取), this)) , m_stopBtn(new QPushButton(QStringLiteral(停止), this)) , m_infoEdit(new QTextEdit(this)) , m_reader(nullptr) { setWindowTitle(QStringLiteral(CVR-100UC 身份证阅读器)); resize(520, 680); m_stopBtn-setEnabled(false); m_infoEdit-setReadOnly(true); auto *deviceRow new QHBoxLayout; deviceRow-addWidget(m_deviceEdit, 1); deviceRow-addWidget(m_startBtn); deviceRow-addWidget(m_stopBtn); auto *mainLayout new QVBoxLayout; mainLayout-addLayout(deviceRow); mainLayout-addWidget(m_infoEdit, 1); auto *central new QWidget(this); central-setLayout(mainLayout); setCentralWidget(central); connect(m_startBtn, QPushButton::clicked, this, MainWindow::startRead); connect(m_stopBtn, QPushButton::clicked, this, MainWindow::stopRead); } void MainWindow::startRead() { if (m_reader) { return; } const QString devicePath m_deviceEdit-text().trimmed(); m_reader new IdCardReader(devicePath, this); connect(m_reader, IdCardReader::dataReady, this, MainWindow::onDataReady); connect(m_reader, IdCardReader::openFailed, this, MainWindow::onOpenFailed); m_reader-start(); m_infoEdit-clear(); m_infoEdit-append(QStringLiteral(正在监听设备%1).arg(devicePath)); m_startBtn-setEnabled(false); m_stopBtn-setEnabled(true); } void MainWindow::stopRead() { if (m_reader) { m_reader-stop(); m_reader-wait(); m_reader-deleteLater(); m_reader nullptr; } m_startBtn-setEnabled(true); m_stopBtn-setEnabled(false); } void MainWindow::onOpenFailed(const QString reason) { m_infoEdit-append(QStringLiteral(打开设备失败%1).arg(reason)); stopRead(); } void MainWindow::onDataReady(const QByteArray data) { IdCardInfo info; if (parseIdCardData(data, info)) { m_infoEdit-append(QStringLiteral( 读卡成功 )); m_infoEdit-append(QStringLiteral(姓名%1).arg(info.name)); m_infoEdit-append(QStringLiteral(性别%1).arg(info.gender)); m_infoEdit-append(QStringLiteral(民族%1).arg(info.nation)); m_infoEdit-append(QStringLiteral(出生%1).arg(info.birth)); m_infoEdit-append(QStringLiteral(住址%1).arg(info.address)); m_infoEdit-append(QStringLiteral(身份证号%1).arg(info.idNumber)); m_infoEdit-append(QStringLiteral(签发机关%1).arg(info.authority)); m_infoEdit-append(QStringLiteral(有效期限%1).arg(info.validity)); } else { QString hexStr; for (int i 0; i data.size(); i) { hexStr QStringLiteral(%1 ) .arg(static_castquint8(data.at(i)), 2, 16, QLatin1Char(0)); if ((i 1) % 16 0) { hexStr QStringLiteral(\n); } } m_infoEdit-append( QStringLiteral(读取到数据但未通过身份证号校验原始字节\n%1\n) .arg(hexStr)); } }这里有一个细节值得单独说一下解析失败时我没有把数据直接丢弃而是把原始字节以十六进制格式打印到界面上。这个看起来不起眼的功能其实是调试协议时最有用的工具。当你面对一个字段偏移不确定的设备时读一次卡、看一次hex几轮下来就能把每个字段的位置对齐比自己闷头猜高效得多。主函数和工程文件#include QApplication #include mainwindow.h int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; w.show(); return app.exec(); }QT core gui widgets TARGET idcard_reader TEMPLATE app CONFIG c11 SOURCES \ main.cpp \ mainwindow.cpp \ idcardreader.cpp \ idcardparser.cpp HEADERS \ mainwindow.h \ idcardreader.h \ idcardinfo.h \ idcardparser.h3.5 完整工程结构与编译运行到这一步整个工程一共是这些文件main.cpp、mainwindow.h/mainwindow.cpp、idcardreader.h/idcardreader.cpp、idcardinfo.h、idcardparser.h/idcardparser.cpp再加上一个idcard_reader.pro工程文件。编译命令就两条/opt/qt5.9.9/bin/qmake idcard_reader.pro make生成的idcard_reader就是ARM版可执行文件。拷贝到开发板先手动确认几件事第一步看hidraw节点是否存在第二步设置QT_QPA_PLATFORM和字体目录第三步运行程序。如果界面能正常弹出点击“开始读取”把身份证放到阅读器感应区正常情况下QTextEdit里会依次显示身份信息。如果一切正常整个链路就算打通了。剩下的就是根据实际业务场景去改界面、加按钮、对接数据库或者把读取到的数据封装成接口提供给上层应用。4. 实测常见问题与排查技巧这部分我按“现象、原因、解法”一条条写基本是实际调试中反复遇到的坑。4.1 插上读卡器/dev下找不到hidraw节点这是最常见的问题。先执行lsusb看看系统有没有识别到USB设备。如果lsusb里根本看不到设备大概率是USB口供电不足或者接触不良换一个USB口试是最快的验证方式。如果lsusb能看到设备但/dev下没有新的hidraw节点检查内核是否开启了USB HID支持确认CONFIG_USB_HID这个配置是否为y或者直接看dmesg最后几十行有没有报错。还有一类情况是开发板的USB HOST口在设备树里默认没打开。正点原子的很多板子需要手工修改设备树使能对应的USB控制器重新编译设备树再烧录。这类问题在正点原子社区里讨论很多结合自己板子的型号搜索关键词基本都能找到方案。4.2 设备节点一直在变开发板上同时插了鼠标、键盘、USB摄像头时hidraw编号不固定程序写死/dev/hidraw0可能过段时间就失效。解决办法是写一个设备枚举函数遍历/sys/class/hidraw/hidraw*/device/uevent根据HID_NAME或者VID/PID锁定目标节点。这比手动改路径可靠得多。我平时会在工程里加一个简单的启动脚本开机时自动把匹配到的节点写到配置文件里程序启动时读取配置文件。虽然多了一步但避免了每次插拔设备后都要重新指定节点的问题。如果你的产品会长期运行这一步建议早点做。4.3 一直收不到数据如果代码运行正常、设备节点也正确但放上身份证后没有任何数据过来先考虑设备是否需要主动命令触发。比较好的做法是把逻辑改成循环发送读卡命令帧再等待设备响应。在Linux下可以用usbmon抓包或者先在Windows上用USB抓包工具看看厂商SDK到底发了什么字节、收回了什么字节然后把同样的字节通过hidraw的write发过去。这个方法看起来很笨实际排查效率非常高。还有一种可能协议帧被拆包了。一次read只拿到部分数据程序直接解析肯定失败。这种情况需要在IdCardReader里维护一个缓冲区把多次read到的字节拼起来根据长度字段判断完整帧再交给解析模块。多帧拼接的逻辑不复杂但能解决很多“时好时坏”的玄学问题。4.4 中文乱码中文乱码要分两种情况看。第一种是界面上的按钮、标题乱码这通常是QT字体目录配置不对或者中文字体没有放到开发板。第二种是读出来的姓名、地址乱码这是GBK转码没做对检查解析代码里是否用了QTextCodec::codecForName(GBK)。我刚开始调试时就是忘了转码姓名显示成一串奇怪字符后来把原始字节打出来才发现中文字节是按GBK编码的才反应过来。还要注意一个细节字符数组里往往带\0直接转码会留下空字符显示的时候看起来像一堆空格甚至乱码。decodeGBK函数里我特意做了尾部裁剪这一步别省。4.5 权限不够open返回Permission denied开发板上的udev规则如果没给hidraw节点足够权限普通用户open会失败。临时解法是chmod 666 /dev/hidraw0一劳永逸的做法是在/etc/udev/rules.d/目录下新增规则文件KERNELhidraw*, SUBSYSTEMhidraw, MODE0666保存后执行udevadm control --reload-rules再重新插拔设备就可以生效。4.6 问题速查表现象可能原因排查方向无/dev/hidraw节点USB HID未启用或USB口供电不足lsusb、dmesg、设备树配置open失败Permission deniedudev权限不足添加rules规则并reload放卡后无数据需要主动命令或收发未对齐用usbmon对比SDK数据流数据乱码GBK转码缺失或字体缺失检查QTextCodec和字体目录解析后身份证号不对字段偏移不符打开hex调试调整offset界面卡死read阻塞在主线程改用子线程加select轮询5. 设备上线前必须处理的隐私与合规问题这一部分我单独拎出来是因为它和“能不能把设备真正交付”直接相关。身份证阅读器读出来的是完整的公民身份信息姓名、身份证号、住址、签发机关每一样都属于敏感个人信息。项目如果只是自己练手、测试问题不大但如果你要把这套方案做成产品或者帮客户部署到酒店、园区、政务窗口就得提前想清楚几件事。第一使用场景必须合法合规。身份证阅读器的使用范围、数据采集目的、保存期限相关法规都有明确要求。项目启动之前先确认业务本身有没有合法授权别等到上线之后才发现采集本身不合规。第二展示和存储要遵循最小化原则。界面上该展示的字段展示不需要的字段别堆出来身份证号这种字段能脱敏展示的尽量脱敏比如只显示前四位和后四位。日志里也不要完整打印身份证号我在调试时打印hex是为了定位协议问题正式交付前一定要把这类调试输出移除。第三数据存储要有保护措施。如果需要把身份证信息写入数据库或者上传服务器优先考虑加密存储或脱敏后再落库。开发板的存储介质通常没有太强的物理防护数据一旦被拷走会很麻烦。代码层面至少做到不明文保存、不随意外发、访问权限最小化。这些经验不是产品经理教我的而是项目交付之后真实踩过坑才总结出来的。希望你在部署第二个项目之前就能看到这几条。最后分享一个我个人的调试习惯。拿到任何一台新的身份证阅读器我第一件事不是写界面而是先写一个几十行的终端小工具把hidraw读到的所有字节以hex形式打印出来然后拿着身份证反复测试。等把数据格式完全摸清再开始写解析和界面。很多人一上来就铺开整套QT工程结果解析不对、偏移不准改起来非常痛苦。先花半小时弄清协议后面至少省两天排查时间。
返回列表