更新记录

1.0.0(2026-09-05)

  • 首个版本
  • 支持居民身份证、港澳台居民居住证、外国人永久居留身份证(2017/2023 版)阅读
  • 支持 USB(默认 vid=1024/pid=50010)与串口两种传输协议
  • 支持单次读卡、自动读卡、USB 插拔事件监听

平台兼容性

uni-app(3.8.3)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
× × 6.0 × ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × - × ×

uni-app x(3.8.3)

Chrome Safari Android Android插件版本 iOS 鸿蒙 微信小程序
× × 6.0 1.0.0 × × ×

zy-zkidcardreader

身份证阅读器 UTS 插件(Android)新中新 华视 神盾 神思 熵基等

简介

离线身份证阅读器UTS插件,支持居民身份证、港澳台居民居住证、外国人永久居留身份证阅读,支持USB和串口两种传输协议,可读取文字、照片(base64)及指纹特征。

支持读取:

  • 居民身份证(二代证)
  • 港澳台居民居住证
  • 外国人永久居留身份证(2017 版 / 2023 版)

支持获取:文字信息、照片(base64)、指纹特征(需授权 SAM 及卡片含指纹)。

支持两种传输协议(initModulemode 参数):

mode 说明 参数
usb(默认) USB HID,需系统 USB 授权 vid(默认 1024)/ pid(默认 50010)
serial 串口,无需 USB 授权 serialName(默认 /dev/ttyS1)/ baudRate(默认 115200)

平台兼容

Android iOS HarmonyOS Web 小程序
x x x x
  • Android 最低版本:21(minSdkVersion 21)
  • 支持 armeabi-v7a / arm64-v8a

使用前提

  1. 本插件依赖中控智慧官方 SDK 的 .so 库(libwlt2bmp / libzkserialport / libzkwltdecode),必须使用自定义基座或云打包生效,标准基座无法运行;
  2. USB 模式下首次初始化会弹出系统 USB 权限弹窗,请选择"允许";
  3. 插件根目录 utssdk/app-android/libs 已内置 SDK 的 jar 与 so,无需额外配置。

API

initModule(options: InitOptions)

设备初始化。

import { initModule } from '@/uni_modules/zy-zkidcardreader'

// USB 模式(默认)
initModule({
  mode: 'usb', // 可省略,默认 usb
  success: (res) => {
    // res.code == 0 初始化成功,res.samId 为安全模块号
  },
  fail: (err) => {
    // err.errCode / err.errMsg
  }
})

// 串口模式
initModule({
  mode: 'serial',
  serialName: '/dev/ttyS1',
  baudRate: 115200,
  success: (res) => { }
})

readCard(options: ReadCardOptions)

单次读卡:在 timeout(默认 20000ms)时间内轮询寻卡,读到后立即回调。

import { readCard } from '@/uni_modules/zy-zkidcardreader'

readCard({
  timeout: 20000,
  success: (res) => {
    // res.name 姓名 / res.sex 性别 / res.nation 民族
    // res.birth 出生日期(yyyyMMdd) / res.address 住址
    // res.idCard 身份证号 / res.depart 签发机关 / res.validityTime 有效期限
    // res.photo 照片 base64(data:image/png;base64,xxx)
    // res.type 证件类型代码 / res.typeName 证件类型名称
  },
  fail: (err) => { }
})

autoReadCard(options: AutoReadCardOptions)

自动读卡:启动后台轮询线程,每读到一张卡通过 onEvent 回调。

import { autoReadCard, stopAutoRead } from '@/uni_modules/zy-zkidcardreader'

autoReadCard({
  interval: 500, // 轮询间隔,默认 500ms,最小 100
  dedup: true,   // 同卡去重,默认 true(同一张卡只回调一次)
  success: (res) => {
    // res.event == 'started' 启动成功
  },
  onEvent: (res) => {
    if (res.event == 'card') {
      // 读到卡,字段同 readCard 的 success
    } else if (res.event == 'stopped') {
      // 自动读卡已停止
    }
  }
})

// 停止
stopAutoRead((res) => { })

close(callback)

关闭设备并释放资源。

import { close } from '@/uni_modules/zy-zkidcardreader'

close((res) => { })

setEventListener(callback)

USB 插拔事件监听(仅 usb 模式有事件):res.eventusbAttached / usbDetached

import { setEventListener } from '@/uni_modules/zy-zkidcardreader'

setEventListener((res) => {
  if (res.event == 'usbDetached') {
    // 阅读器被拔出,可提示用户重新插拔后调用 initModule
  }
})

错误码

errCode 说明
5010001 参数错误
5010002 未检测到身份证阅读器
5010003 USB 权限被拒绝
5010004 设备未初始化
5010005 初始化超时
5010006 打开设备失败
5010007 读卡超时
5010008 解析证件信息失败
5010009 设备已关闭
5010010 上下文为空

示例代码

<template>
    <view class="container">
        <view class="header">
            <text class="title">身份证阅读器</text>
            <text class="subtitle">身份证阅读器(新中新/华视/神盾/神思/熵基等)</text>
        </view>

        <!-- 设备状态 -->
        <view class="section">
            <text class="section-title">设备状态</text>
            <view class="status-row">
                <text class="status-label">连接状态:</text>
                <text class="status-value" :class="opened ? 'status-on' : 'status-off'">{{ statusText }}</text>
            </view>
            <view class="status-row" v-if="samId">
                <text class="status-label">SAM卡号:</text>
                <text class="status-value">{{ samId }}</text>
            </view>
            <view class="status-row" v-if="usbEvent">
                <text class="status-label">USB事件:</text>
                <text class="status-value">{{ usbEvent }}</text>
            </view>
        </view>

        <!-- 初始化 -->
        <view class="section">
            <text class="section-title">设备初始化</text>
            <radio-group class="mode-group" @change="onModeChange">
                <label class="mode-item">
                    <radio value="usb" :checked="mode === 'usb'" color="#007aff" />
                    <text>USB(默认 1024/50010)</text>
                </label>
                <label class="mode-item">
                    <radio value="serial" :checked="mode === 'serial'" color="#007aff" />
                    <text>串口(/dev/ttyS1)</text>
                </label>
            </radio-group>
            <view class="button-group">
                <button type="primary" size="mini" @click="doInit" :loading="initializing">初始化</button>
                <button type="warn" size="mini" @click="doClose">关闭设备</button>
            </view>
        </view>

        <!-- 读卡 -->
        <view class="section">
            <text class="section-title">读卡操作</text>
            <view class="option-row">
                <checkbox :checked="readFingerprint" color="#007aff" @tap="readFingerprint = !readFingerprint" />
                <text class="option-text">读取指纹特征(需授权SAM)</text>
            </view>
            <view class="button-group">
                <button type="primary" size="mini" @click="doReadCard" :disabled="!opened || autoRunning">单次读卡</button>
                <button v-if="!autoRunning" type="primary" size="mini" @click="doAutoRead" :disabled="!opened">自动读卡</button>
                <button v-else type="warn" size="mini" @click="doStopAuto">停止自动</button>
            </view>
        </view>

        <!-- 读卡结果 -->
        <view class="section" v-if="card">
            <text class="section-title">读卡结果({{ card.typeName || '证件' }})</text>
            <view class="photo-wrap" v-if="card.photo">
                <image class="photo" :src="card.photo" mode="aspectFit" />
            </view>
            <view class="result-item">
                <text class="label">姓名:</text>
                <text class="value">{{ card.name || '-' }}</text>
            </view>
            <view class="result-item">
                <text class="label">性别:</text>
                <text class="value">{{ card.sex || '-' }}</text>
            </view>
            <view class="result-item" v-if="card.nation">
                <text class="label">民族:</text>
                <text class="value">{{ card.nation }}</text>
            </view>
            <view class="result-item" v-if="card.country">
                <text class="label">国籍:</text>
                <text class="value">{{ card.country }}</text>
            </view>
            <view class="result-item">
                <text class="label">出生:</text>
                <text class="value">{{ card.birth || '-' }}</text>
            </view>
            <view class="result-item" v-if="card.address">
                <text class="label">住址:</text>
                <text class="value">{{ card.address }}</text>
            </view>
            <view class="result-item">
                <text class="label">证件号:</text>
                <text class="value">{{ card.idCard || '-' }}</text>
            </view>
            <view class="result-item" v-if="card.depart">
                <text class="label">签发机关:</text>
                <text class="value">{{ card.depart }}</text>
            </view>
            <view class="result-item" v-if="card.validityTime">
                <text class="label">有效期:</text>
                <text class="value">{{ card.validityTime }}</text>
            </view>
            <view class="result-item" v-if="card.fingerprintLength > 0">
                <text class="label">指纹:</text>
                <text class="value">{{ card.fingerprintLength }} 字节特征数据</text>
            </view>
        </view>

        <!-- 日志 -->
        <view class="section">
            <text class="section-title">操作日志</text>
            <view class="log-box">
                <text class="log-line" v-for="(log, index) in logs" :key="logIndex - index">{{ log }}</text>
            </view>
            <view class="button-group" style="margin-top: 16rpx;">
                <button type="default" size="mini" @click="logs = []">清空日志</button>
            </view>
        </view>
    </view>
</template>

<script>
    import { initModule, readCard, autoReadCard, stopAutoRead, close, setEventListener } from '@/uni_modules/zy-zkidcardreader'

    export default {
        data() {
            return {
                mode: 'usb',
                readFingerprint: false,
                initializing: false,
                opened: false,
                autoRunning: false,
                samId: '',
                usbEvent: '',
                card: null,
                logs: [],
                logIndex: 0
            }
        },
        computed: {
            statusText() {
                return this.opened ? '已连接' : (this.initializing ? '初始化中...' : '未连接')
            }
        },
        onUnload() {
            // 页面卸载时停止自动读卡并关闭设备
            stopAutoRead()
            close()
        },
        methods: {
            addLog(msg) {
                const now = new Date()
                const pad = n => (n < 10 ? '0' + n : '' + n)
                const time = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds())
                this.logs.unshift('[' + time + '] ' + msg)
                this.logIndex++
                if (this.logs.length > 100) {
                    this.logs.length = 100
                }
            },
            onModeChange(e) {
                this.mode = e.detail.value
            },
            doInit() {
                if (this.initializing) return
                this.initializing = true
                this.addLog('开始初始化(' + this.mode + ' 模式)...')
                initModule({
                    mode: this.mode,
                    readFingerprint: this.readFingerprint,
                    success: (res) => {
                        this.initializing = false
                        this.opened = true
                        this.samId = res.samId || ''
                        this.addLog('初始化成功: ' + (res.msg || '') + (res.samId ? (',SAM=' + res.samId) : ''))
                        // 注册 USB 插拔事件(仅 usb 模式有事件)
                        setEventListener((evt) => {
                            this.usbEvent = evt.event
                            if (evt.event === 'usbDetached') {
                                this.opened = false
                                this.addLog('阅读器已拔出,请重新初始化')
                            } else if (evt.event === 'usbAttached') {
                                this.addLog('阅读器已接入')
                            }
                        })
                    },
                    fail: (err) => {
                        this.initializing = false
                        this.opened = false
                        this.addLog('初始化失败: [' + err.errCode + '] ' + err.errMsg)
                        uni.showToast({ title: err.errMsg, icon: 'none' })
                    }
                })
            },
            doClose() {
                stopAutoRead()
                this.autoRunning = false
                close((res) => {
                    this.opened = false
                    this.card = null
                    this.addLog('设备已关闭')
                })
            },
            doReadCard() {
                this.addLog('开始读卡(请放置卡片)...')
                readCard({
                    timeout: 20000,
                    readFingerprint: this.readFingerprint,
                    success: (res) => {
                        this.card = res
                        this.addLog('读卡成功: ' + (res.name || '') + ' ' + (res.idCard || ''))
                    },
                    fail: (err) => {
                        this.addLog('读卡失败: [' + err.errCode + '] ' + err.errMsg)
                    }
                })
            },
            doAutoRead() {
                autoReadCard({
                    interval: 500,
                    dedup: true,
                    readFingerprint: this.readFingerprint,
                    success: (res) => {
                        this.autoRunning = true
                        this.addLog('自动读卡已启动(同卡去重),取走卡片后重新放卡会再次回调')
                    },
                    fail: (err) => {
                        this.addLog('启动失败: [' + err.errCode + '] ' + err.errMsg)
                    },
                    onEvent: (res) => {
                        if (res.event === 'card') {
                            this.card = res
                            this.addLog('读到卡片: ' + (res.name || '') + ' ' + (res.idCard || ''))
                        } else if (res.event === 'stopped') {
                            this.autoRunning = false
                            this.addLog('自动读卡已停止')
                        }
                    }
                })
            },
            doStopAuto() {
                stopAutoRead((res) => {
                    this.addLog('请求停止自动读卡')
                })
            }
        }
    }
</script>

<style>.container {padding: 24rpx;}.header {margin-bottom: 24rpx;}.title {font-size: 40rpx;font-weight: bold;color: #333;}.subtitle {display: block;margin-top: 8rpx;font-size: 26rpx;color: #999;}.section {background: #fff;border-radius: 16rpx;padding: 24rpx;margin-bottom: 24rpx;}.section-title {display: block;font-size: 30rpx;font-weight: bold;color: #333;margin-bottom: 16rpx;}.status-row {display: flex;margin-bottom: 8rpx;}.status-label {width: 160rpx;font-size: 26rpx;color: #666;}.status-value {flex: 1;font-size: 26rpx;color: #333;word-break: break-all;}.status-on {color: #07c160;}.status-off {color: #fa5151;}.mode-group {margin-bottom: 16rpx;}.mode-item {display: flex;align-items: center;margin-bottom: 12rpx;}.mode-item text {margin-left: 8rpx;font-size: 26rpx;color: #333;}.option-row {display: flex;align-items: center;margin-bottom: 16rpx;}.option-text {margin-left: 8rpx;font-size: 26rpx;color: #333;}.button-group {display: flex;flex-direction: row;gap: 16rpx;}.photo-wrap {display: flex;justify-content: center;margin-bottom: 16rpx;}.photo {width: 204rpx;height: 264rpx;border: 1rpx solid #eee;border-radius: 8rpx;}.result-item {display: flex;margin-bottom: 10rpx;}.result-item .label {width: 180rpx;font-size: 26rpx;color: #666;}.result-item .value {flex: 1;font-size: 26rpx;color: #333;word-break: break-all;}.log-box {background: #f7f7f7;border-radius: 8rpx;padding: 16rpx;max-height: 400rpx;overflow-y: scroll;}.log-line {display: block;font-size: 22rpx;color: #666;line-height: 36rpx;word-break: break-all;}</style>

注意事项

  1. 指纹读取:需同时满足 ① readFingerprint: true;② 卡片含指纹(2013 年后采集过指纹的二代证);③ SAM 模块具备指纹解密授权,否则 fingerprintLength 恒为 0;
  2. 单次读卡与自动读卡互斥:调用 readCard 会自动停止自动读卡;
  3. 重复调用 initModule 会先释放上一次的资源再重新初始化;
  4. 阅读器 USB 拔出后需重新插拔并再次调用 initModule
  5. 项目中含 .so 的 UTS 插件需使用自定义基座运行或云打包。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

无(USB设备访问采用系统运行时授权,AndroidManifest已声明usb.host特性)

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

身份证数据仅用于本地离线读取,不会上传至服务器

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。