1. 项目背景与整体设计思路
先交代一下背景。我手上这个项目是公司内部的园区一卡通升级,原来是一套原生 Android 的读卡 App,负责门禁刷卡、食堂扣款、会议室签到这些事。后来业务要求同时覆盖 iOS 和微信小程序,团队又不想维护三套代码,于是决定用 uni-app 重构。听起来很常规,但 NFC 这个模块在整个迁移过程中被我们严重低估了,尤其当目标是支持 Mifare Classic 卡片的时候,坑比想象中多得多。这篇文章就是把从需求评审到联调上线这段路踩过的坑、走过的弯路、最终验证可行的方案完整写下来,给准备做 uni-app + NFC 的团队一个参考。
标题里写了“从门禁到支付”,但先把话说在前面:门禁和支付在 NFC 领域的技术底座是完全不同的。门禁大多数走 Mifare Classic 的扇区读写或者仅读取卡号,而真正的支付场景走的是金融 IC 卡规范(比如 EMV/PBOC)或者手机厂商的 Pay 体系,安全级别完全不是一个量级。如果你的项目是把一张 Mifare 1K 卡当“电子钱包”用,本质上属于封闭环境里的小额储值卡,跟银行卡闪付是两码事,后面我会专门讲这块的安全设计思路。
1.1 一张门禁卡背后的技术构成
先通俗地把底层概念捋一遍。我们常说的“刷卡”,本质上是读卡器和卡片之间通过射频天线进行无线通信。RFID 是射频识别的总称,常见的有低频 125kHz、高频 13.56MHz、超高频 900MHz 这些频段;而 NFC 是工作在 13.56MHz 的一种近距离无线通信技术,兼容了 ISO 14443 A/B、ISO 15693、FeliCa 等一系列标准。换句话说,NFC 是 RFID 的一个子集,但标准化程度更高、生态更成熟。
Mifare Classic 则是 NXP 公司基于 ISO 14443A 标准实现的一套卡片芯片方案。市面上大量小区门禁、校园一卡通、园区工牌用的就是 Mifare Classic 1K 卡。这张卡内部结构非常有特点:总共 16 个扇区,每个扇区 4 个块,每块 16 字节。第 0 扇区第 0 块是厂商数据区,包含 4 字节 UID、校验位和厂商信息,这张卡的“身份证号”就在这里;每个扇区的最后一个块是扇区尾块,存放 KeyA、访问控制位和 KeyB,相当于这扇门的钥匙锁结构;中间的块则可以存放业务数据。
这个结构决定了门禁读卡有两种常见模式:一种只读 UID,把卡号当成身份标识,这种最简单,系统里存一个白名单就行;另一种要读扇区数据,卡片里存着用户编号、有效期、余额甚至楼层权限,读卡器拿到数据后还要做密钥认证和解密,这明显更复杂。搞清楚你的业务属于哪一种,比急着写代码重要得多。
1.2 门禁与支付,技术底座完全不是一回事
很多产品经理提需求的时候会说“一张卡既能开门又能付钱”,听起来好像一个 NFC 读写功能就全覆盖了,实际上开发层面的难度是成倍增加的。门禁场景对安全的要求是“能验明身份”,多数情况下读个 UID 就够了,即使要读扇区,密钥掌握在物业侧,风险可控。
支付场景却完全不同。银联闪付、Apple Pay、各种手机 Pay 的本质是把银行卡的敏感数据放在安全芯片 SE 里,用 token 化技术和动态密钥完成交易,每次支付生成一个一次性的交易凭证。而 Mifare Classic 使用的是 Crypto-1 流密码算法,这套算法在多年前就被安全社区公开分析过,密钥体系早已不是秘密,所以它根本扛不住金融级的安全评估。
我在项目里跟业务方反复确认过“支付”到底指什么,最终确定是园区食堂的小额储值扣款,卡片余额最多几百块。这个场景还能做,但绝不能把余额直接存在手机端让前端改写,而是要把手机变成“身份凭证”,真正的资金操作放服务端。这个认知是整个架构设计的基石,建议每个团队在立项前都花半天把需求边界谈清楚。
1.3 方案选型:三类需求,三条完全不同的路
我在预研阶段把 NFC 需求拆成了三类,每一类的实现路径截然不同,技术选型必须先对号入座。
| 需求类型 | 典型场景 | iOS 可行性 | Android 可行性 | uni-app 实现难度 |
|---|---|---|---|---|
| 只读 UID / 卡号 | 门禁白名单、签到、设备绑定 | 可行,CoreNFC 可读取标签标识 | 可行,MifareClassic.getUID() 即可 | 低,封装插件即可 |
| 读写 NDEF 数据 | 智能海报、蓝牙配对、配置标签 | 可行,CoreNFC 原生支持 NDEF | 可行,Ndef 类可读写 | 中,注意数据格式兼容 |
| 扇区机密读写 | 门禁楼层权限、一卡通余额存储 | iOS 受限,系统未开放扇区访问 | 可行,需密钥认证后 readBlock/writeBlock | 高,必须原生插件或 UTS |
读到这里你应该明白了,uni-app 虽然口号是“一套代码多端运行”,但 NFC 这种底层硬件能力在不同系统上的开放程度完全不一样,写代码前先判断需求属于哪一类,能帮你节省大量返工时间。我们项目的核心门禁功能同时涉及 UID 读取和扇区权限判断,Android 端可以做完整实现,iOS 端最终采用的是读 UID + 服务端二次校验的方案,这个妥协方案后面详细说。
2. 环境准备与关键配置
uni-app 开发 NFC 的第一道坎并不是代码本身,而是环境配置。很多人跟我一样,刚开始以为在 manifest.json 里勾选一个权限就能跑,结果真机一测才发现连 NFC 适配器都拿不到。这里面的门道,值得花一整章讲清楚。
2.1 manifest.json 权限声明:我在这里翻过车
先说结论:uni-app 在 App 端并没有像 uni.scanCode 那样开箱即用的 NFC 接口,它需要依赖原生插件或 UTS 插件去调用系统能力。因此 manifest.json 的配置要做到两步:声明原生权限,同时把插件模块勾上。
// manifest.json 的 app-plus 节点里需要做如下配置 "app-plus": { "modules": { "NFC": {} }, "distribute": { "android": { "permissions": [ "<uses-permission android:name=\"android.permission.NFC\"/>", "<uses-feature android:name=\"android.hardware.nfc\" android:required=\"true\"/>" ] }, "ios": { "privacyDescription": { "NFCReaderUsageDescription": "需要使用NFC功能读取门禁卡/一卡通信息" } } } }Android 端要注意的是,NFC 权限在系统里属于 normal 级别权限,不需要像定位、相机那样做运行时动态申请,但必须确保uses-feature声明了android.hardware.nfc,否则部分应用市场会认为你的应用不支持 NFC,甚至连安装都不让装。另外如果只是辅助功能,可以把required设为true,这样商店会主动过滤掉没有 NFC 硬件的设备;如果 NFC 只是锦上添花,就设false。
iOS 端则要复杂一些。除了在 manifest.json 里加NFCReaderUsageDescription描述文案之外,离线打包时必须在 Xcode 工程里开启 Capability:Near Field Communication Tag Reading,并且要在 entitlements 文件里配置对应的 key。云打包稍微省心一点,但同样需要在打包配置里确认勾选。这个描述文案会被系统在用户第一次触发 NFC 功能时显示,务必写清楚用途,写得含糊会被审核驳回。
2.2 UTS 插件和原生插件怎么选
配置好权限之后,紧接着就是技术选型。uni-app 生态里做 NFC 有三条路:去插件市场买现成的原生插件、自己写原生插件让前端调用、用 UTS 插件把原生能力封装成前端可以直接 import 的模块。我三样都试过,简单聊聊各自的感受。
插件市场里的现成插件适合快速验证原型,通常支持 Mifare Classic 的读写、NDEF 解析这些主流能力,价格从免费到几百块不等。但问题在于闭源,遇到 bug 只能等作者修,而且不同插件的 API 风格差异很大,后期换插件等于重写业务层。我们上生产环境前发现一个插件在 Android 13 上读取速度很慢,最后被迫换方案,这笔时间成本很高。
自己写原生插件灵活性最高,但开发成本和维护成本也不低,每次 uni-app 升级、原生 SDK 升级都要重新打包测试,而且团队里得有同时懂 Android、iOS 和 uni-app 生态的人。我们最后用的是 UTS 插件方案,核心逻辑用 TS 写,可以直接访问 Android 和 iOS 的原生 API,又能跟 uni-app 的页面代码共享类型定义,算是在灵活性和可维护性之间取了平衡。
// UTS 插件内部可以这样封装原生调用,前端 JS 层直接 import import { NfcAdapter } from 'android.nfc'; import { Intent } from 'android.content'; export function getNFCAvailable(): boolean { const adapter = NfcAdapter.getDefaultAdapter(uni.getSystemInfoSync().platform === 'android' ? uni.getActivity() : null); return adapter != null && adapter.isEnabled(); }上面的代码是 UTS 开发的一个示意,具体 API 映射要以你依赖的 uni-app 版本和运行环境为准。核心思路是:UTS 在编译期会把你写的 TS 转为对应平台的原生代码,所以 Android 端能用 Kotlin/Java 的 API,iOS 端能用 Swift/OC 的能力,前端只管调用统一封装好的 JS 接口,这个模式非常适合 NFC 这种强平台相关的能力。
2.3 自定义基座、离线打包与真机调试
NFC 的调试比普通页面调试麻烦,因为权限和原生模块必须打包进基座才生效。用 HBuilderX 自带的标准基座跑普通项目没问题,但涉及 NFC 这类原生能力,你必须先做一次自定义基座。操作不复杂:在 manifest.json 配置完成后,点击“自定义调试基座”构建,然后把自定义基座安装到手机,运行项目时选择这个基座即可。
这里有个特别容易踩的坑:改了 manifest.json 的原生配置后,如果忘了重新打包基座,你会发现无论如何都拿不到 NFC 适配器,因为运行时的基座里根本没有打进这个模块。我们团队新人接手项目时至少两次栽在这上面,排查半天最后发现只是基座没更新。
离线打包则是另一个维度的事。如果你的应用需要通过原生工程做特殊集成,比如接入厂商推送、定制系统级能力,就会走到离线打包这条路。离线打包时 UTS 插件的使用步骤大致是:先在 uni-app 项目中开发好 UTS 插件,然后通过 HBuilderX 生成离线打包资源,再把资源放入原生工程,同时把 UTS 插件对应的原生源码或 aar 引入到工程依赖里。这一步的细节比较多,强烈建议参照官方离线打包文档一步步来,不要凭记忆操作。我自己的习惯是先用云打包验证功能,确认无误后再走离线打包流程,这样可以隔离变量、快速定位问题。
3. NFC 核心功能实战
环境搭好之后,真正的硬骨头来了。这一章我会按读 UID、扇区读写、门禁联动、支付安全这四个模块,把实际开发中验证过可行的流程完整写出来。
3.1 读卡流程:从开始扫描到拿回 UID
读 UID 是整个 NFC 功能里最基础、也最常用的能力。不管是门禁白名单、设备绑定,还是判断卡片是否存在,第一步都是先把卡片身份拿到手。Android 端的原生机制是注册一个 Intent 过滤器,当 NFC 标签靠近时系统会弹出标签调度,把 Tag 对象传递给你的 Activity;开发时建议用前台调度(PendingIntent)方式,避免读卡时跳出当前页面。
// 示意:前端通过 UTS 封装后的插件调用扫描 const nfc = uni.requireNativePlugin('NFCModule'); nfc.startScan({ mode: 'UID_ONLY', timeout: 30000 }, (result) => { if (result.code === 0) { console.log('读到的卡号是:', result.cardId); this.cardId = result.cardId; } else { uni.showToast({ title: '未识别到卡片', icon: 'none' }); } });上述是业务侧的调用示意,真正复杂的是原生层对 Intent 的处理。在原生代码里,你要在onNewIntent中接收EXTRA_TAG参数,然后Tag.getTechList()判断标签支持的技术类型。如果列表里有MifareClassic,就可以强转成 MifareClassic 对象并调用getUID()方法拿到字节数组,再转成十六进制字符串供业务使用。
一个非常实用的经验:不要每次读卡都重新启动 Activity,尽量把 NFC 相关逻辑放到单例或常驻的 Service 里,用生命周期管理 Intent 的传递。我们早期版本因为 Intent 重复调度导致页面卡顿,后来改成了在onResume时执行 enableForegroundDispatch,在onPause时 disable,彻底解决了问题。
读卡期间还要考虑用户体验。用户把卡贴上去可能只停留几百毫秒,读卡器必须在这个窗口内完成发现、连接、认证、读取整个流程。建议在页面上做一个明显的“读卡中”状态提示,同时在代码里做好超时处理和异常捕获,避免碰到一次不支持的卡片就白屏卡死。
3.2 Mifare Classic 扇区读写:密钥是绕不过去的一关
说完了读 UID,接下来是重头戏:Mifare Classic 的扇区读写。前面提到过,1K 卡有 16 个扇区,每个扇区 4 个块,每个块 16 字节。第 0 扇区第 0 块是厂商数据区,存储 UID,出厂后绝大多数卡片不可修改;每个扇区的最后一块是 Trailer 块,存放 KeyA、KeyB 和访问位。要读取某个数据块,必须先对所在扇区进行密钥认证,否则卡片会直接拒绝访问。
密钥认证和读块操作的逻辑示意: 1. 拿到 Tag 对象,检查是否支持 MifareClassic 2. 连接卡片,调用 connect() 3. 指定扇区号,调用 authenticateSectorWithKeyA(sector, keyBytes) 4. 认证成功后,调用 readBlock(blockIndex) 读取 16 字节数据 5. 处理完数据后,及时调用 close() 释放连接这里面最大的坑是密钥。厂家出厂的 Mifare Classic 卡默认密钥通常有两组:KeyA 全是FF,KeyB 是A0A1A2A3A4A5。如果门禁系统在发卡时没有改写密钥,那这张卡的数据等于对所有人敞开,任何人拿到卡都能读取甚至改写扇区内容。反过来,如果门禁系统已经改写了密钥,那你的应用要想读扇区,就必须在某个环节拿到这把密钥。
安全实践上,绝对不要把这些密钥硬编码在前端代码里,不管是 uni-app 的 JS 层还是原生插件层,密钥写死在包里等于把保险柜钥匙贴在保险柜外面。我们的做法是:用户在应用内完成身份认证后,服务端根据用户权限实时下发一个加密后的会话密钥,App 把这个密钥传给原生模块完成扇区认证,会话过期就失效。密钥下发链路本身用 HTTPS 加密,密钥不落本地数据库,用完后立即从内存中清掉。
另外要注意,Mifare Classic 的扇区读写效率不高,读写过程对时序比较敏感。实测下来,如果 App 在读取过程中突然切后台,或者系统弹出权限弹窗,极容易导致连接中断,卡片状态会出错。所以进入读卡页面前,一定要提醒用户关闭 NFC 相关系统弹窗,并且把页面保持在最前台。我们在代码里还做了重试机制,一次认证失败后自动重试两次,有效提升了体验。
3.3 门禁联动:读卡器之外,你还要考虑这些
门禁场景真正落地时,你会发现“读到卡号”只是万里长征第一步。整个链路是:用户打开 App -> 进入刷卡页面 -> 贴卡 -> 读取 UID/扇区数据 -> 上送服务端校验 -> 服务端返回是否开门 -> App 调用蓝牙或网络模块触发门禁控制器开锁。这里面任何一环卡住,用户就会被关在门外。
一个容易被忽略的细节是卡片的去重与轮询。当用户把卡贴在手机背面时,NFC 的轮询机制可能会在短时间内触发多次 Tag Discovered 事件。如果代码不做去重,服务端会收到连续多个相同的开门请求,这不仅会造成重复开门记录,还可能触发门禁系统的防重入机制。解决办法很简单:在原生层记录上一次读取到的卡号和读取时间,500 毫秒内的重复卡号直接忽略。
iOS 端的门禁方案我需要再展开说一下。由于 iOS 的 CoreNFC 不支持直接访问 Mifare Classic 的扇区数据,只能读取标签的基本信息,所以团队最终采用了“读 UID + 服务端白名单”的架构。手机端 NFC 读取卡片 UID 后,把 UID 和当前用户信息一起上送服务端,服务端判断这个 UID 是否绑定在当前用户名下,并返回开门指令。
这种方案对原生门禁系统比较友好,因为大多数门禁系统本来就保存着卡号和用户的关系。但它也有一个明显缺点:如果门禁系统还要求检查卡内扇区数据(比如有效期、楼层权限),那么 iOS 端就无法完整模拟实体卡。我们的妥协方案是:iOS 端只做“解绑式”开门,即服务端强制校验用户身份,不再依赖卡内扇区数据;如果卡内扇区数据必须一致,那就只能引导用户用实体卡。这个限制要在产品规划阶段就跟业务方讲清楚,否则验收时很容易扯皮。
3.4 支付场景落地的安全设计思路
再回到“支付”这个敏感词。如果业务场景是园区食堂、超市、会议室的小额消费,技术上可以参考一卡通电子钱包的做法,但我的建议非常明确:不要在手机端直接改写卡片余额,不要尝试绕过任何密钥体系,把资金操作全部放到服务端。
合规和安全的设计思路是这样的:卡片在系统里只充当身份凭证,App 读到的 UID 或扇区用户标识用于确认“你是谁”,真正的余额和流水只存在于中心数据库中。扣款流程是用户出示手机上的付款码或者靠近读卡器,读卡器将身份信息上送服务端,服务端开启事务、校验余额、执行扣款、记录流水,再把结果返回给读卡器。卡片本身甚至可以不要余额,完全变成一个身份令牌。
如果在离线环境必须要用卡片余额,比如食堂网络不稳定,那也得把卡片当成只读的“余额快照”,真正的扣款记录在本地事务表里,网络恢复后与中心对账。我们团队最终连这一步都砍掉了,因为对账和防抵赖的复杂度实在太高,一个小数点错误都可能引发客诉。记住一个原则:能用服务端算的钱,绝不在前端算;能用数据库记的账,绝不在卡里记。
还要专门提一下 Mifare Classic 本身的安全劣势。它的 Crypto-1 算法在多年以前就被安全社区逆向分析过,密钥也早已被公开讨论过,这意味着把它用于任何涉及真实资金或高安全权限的系统都需要非常谨慎。正因如此,现在越来越多新项目开始用 CPU 卡(如 Mifare DESFire、复旦微 FM11S 系列)替代 Classic 卡,后者支持更复杂的加密算法和文件权限管理。如果你的项目还在选型阶段,我强烈建议把 CPU 卡列入备选,哪怕单张成本高个一两块钱,也远比以后被安全问题逼迫升级划算。
4. 高频问题的排查速查表
写代码只是开始,真机联调才是地狱。这一章我把这段时间积攒的高频问题和排查经验整理成速查表,每一个都是实际踩坑换来的。
4.1 手机扫描不到卡片,先从这四步查起
“手机明明支持 NFC,为什么就是读不到卡”是群里出现频率最高的问题。我每次排查的顺序固定为四个步骤:先看系统 NFC 开关、再看贴卡位置、然后查手机壳和贴膜、最后查应用进程占用。
第一步,系统 NFC 开关。很多用户买完手机之后 NFC 是默认关闭的,尤其部分国产 ROM 会默认关闭以省电。App 要做的是在进入刷卡页时主动检查系统 NFC 状态,如果未开启,用明确的文案引导用户去系统设置打开,而不是只弹一个“未检测到卡片”的提示。
第二步,贴卡位置。NFC 天线一般在手机背面的上部摄像头附近,不同的机型位置差异很大。如果用户习惯把卡贴在手机正中间,很可能怎么刷都没反应。比较好的做法是在 UI 上画一个手机示意图,标注推荐刷区位置,降低用户试错成本。
第三步,手机壳和贴膜。金属边框、磁吸支架、过厚的硅胶壳都会严重衰减或偏转 NFC 的射频场,我有一次调试几个小时,最后发现是用户新带了一个金属支架壳。排查时不妨让用户把壳摘了试试。
第四步,进程占用。部分 App 如果长期占据 NFC 的读卡轮询,会导致其他应用无法发现标签。遇到读不到卡的情况,尝试清理所有后台应用再测。这一点在定制 ROM 上尤为明显,后面单独讲。
4.2 Android 厂商适配:权限声明之外的大坑
Android 系统碎片化是老生常谈,NFC 领域更是重灾区。除了前面说的 NFC 权限声明,你还得面对厂商自定义的省电策略、自启动限制和弹窗管理。典型的情况是小米、华为、OPPO、vivo 这些厂商默认不允许应用在后台调用 NFC,一旦 App 切到后台再回来,NFC 功能会失效。
开发阶段最直观的体现是:把 App 切后台再切回来,NFC 扫描就“失灵”了,重装才好使,其实是原生连接没有正确释放。解决办法是在页面onShow里重新初始化 NFC 适配器,onHide里主动释放资源。这里也提醒一下,很多团队在反馈里看到“小米手机打包 app 之后为啥没有麦克风权限”“为啥 NFC 用不了”这类问题,拉日志一看,绝大多数都是因为离线打包时原生权限声明不完整,或者运行基座没有重新编译。在验证任何模块问题之前,先确认你运行的是不是最新打包的基座和资源。
厂商 API 差异还体现在卡片交互上。一些三星、谷歌原生系统机型对 Mifare Classic 的支持很标准;但部分国产机型在系统层做过 NFC 轮询优化,会导致读卡灵敏度下降。这类问题没法通过应用层代码完全解决,只能建议用户在系统设置里关闭 NFC 省电模式,同时在读卡页面做明显的引导动画。记得在测试矩阵里覆盖不同厂商的机型,至少保证主流品牌各有一台真机。
4.3 iOS 端的边界:能做什么,不能做什么
iOS 的 CoreNFC 从 iOS 11 开始支持 NDEF 标签读取,到 iOS 13 全面放开,读 UID 和部分协议指令已经可用,但它对 Mifare Classic 的扇区访问始终没有开放。很多从 Android 转过来的开发者容易想当然,以为 iOS 也能一样操作扇区,结果发现调系统 API 只能拿到标签的基础标识,读不出扇区内容。
iOS 端做 NFC 开发前,先确认自己的需求落在系统允许的范围内:
- 可以:读取 NDEF 消息,读取 ISO 14443A/B、FeliCa 等标签的基础能力,读取支持 ISO 7816 APDU 的卡片部分能力。
- 不可以:直接访问 Mifare Classic 的扇区密钥认证和读写。
- 可以但不稳定的情况:部分 iOS 版本读 UID 支持很好,但在老版本或特定机型上可能遇到偶发失败。
所以如果你要同时支持 iOS 和 Android 的 NFC 门禁,最务实的方案是前面说的:Android 做扇区读写,iOS 做 UID 读取 + 服务端校验。如果业务方硬性要求 iOS 也能完整读扇区,那就需要引入一个外接读卡器硬件,通过蓝牙或音频口与 iPhone 通信,这是当前 iOS 生态里唯一可靠的办法。
另外 iOS 的隐私弹窗也容易让人困惑。首次使用 NFC 时,系统会弹出NFCReaderUsageDescription对应的授权框,这个框不是申请权限,而是在告知用户你要使用 NFC 标签读取。如果不弹或者弹完就崩,很大概率是 entitlements 或者 plist 配置缺失,重新检查离线打包工程里的配置即可。
4.4 安全与合规红线:中继攻击、密钥泄露与上线评审
最后这部分,我把它放在“避坑指南”里,是因为很多开发团队在功能实现后就把 NFC 项目当成功了,却忽视了安全和合规层面的风险,等出了事再补救成本极高。
先说行业里常被讨论的“中继攻击”。NFC 的本质是短距离无线通信,正常工作时读卡器和卡片的距离只能有十几厘米,这本身就是一道物理防线。中继攻击的思路是把读卡器的射频信号远程转发给另一台设备上的卡片,相当于把物理距离拉长了,攻击者可以在用户不知情的情况下触发刷卡或支付。这个议题提醒我们,任何仅依赖“卡片靠近”作为唯一认证因素的系统都有被中继的风险。防御方向是引入动态挑战响应机制:门禁读卡器或支付终端在每次交易时生成随机挑战值,卡片或手机端必须用密钥体系计算出正确的响应,只是简单转发信号无法通过校验。另外在风控层面,可以对刷卡频率、刷卡位置、门禁记录做异常检测。
再回到钥匙本身。前面说了 Mifare Classic 的密钥体系在多年以前就被公开分析过,如果你发现你负责的门禁系统还在用出厂默认密钥,或者整栋楼所有卡都共用一个密钥,这属于非常严重的安全隐患。作为技术人员,项目上线前至少要做一次结构性的安全评审:密钥如何生成、如何分发、如何轮换?读卡数据在传输过程中是否加密?服务端是否有针对重复刷卡、越权读卡的风控策略?这些问题的答案应该写进技术文档里,而不是等事后补救。
合规层面,涉及门禁和资金的功能,尤其是校园、园区、社区这类面向大量用户的场景,一定要在需求阶段就和安全团队、法务对齐。不要在 App 里提供任何批量写卡、修改卡内余额、复制他人卡号的能力,即便技术上可行,这也是不能碰的红线。我们最终在 App 里只暴露了“读 UID + 服务端校验”能力,扇区读写全部收敛在发卡器设备端,设备本身也需要管理员权限登录才能操作,最大程度减少被滥用的面。
如果你问我做这个项目最大的心得是什么,我的回答是:NFC 开发的难点从来不在“能不能读到卡”,而在于读懂平台边界、理解卡片协议、尊重安全底线。uni-app 可以做很好的跨端业务层,但底层的 NFC 能力必须拿出足够的耐心去啃原生文档。把这个心态放正,再配合上面这些经验,你的门禁也好、一卡通也好,踩坑数量至少能减掉大半。