用 Express 简单Mock自助终端机读取身份证

12 阅读5分钟

自助终端(办事大厅、医院、政务一体机等)上的业务系统,往往通过本地 HTTP 服务与读卡器、密码键盘等外设交互。真机联调成本高、环境难凑齐时,用 Express 起一个轻量服务,按约定 JSON 返回「开设备、读证、关设备」等结果,就能在浏览器或终端页面里先把流程跑通。本文将介绍如何用 Express 模拟读取二代身份证这一类接口(含 open / getData / close 等动作)。


一、场景与目标:为什么要单独起一个服务?

真实环境里,终端侧软件可能通过固定端口访问本机读卡服务;开发阶段没有硬件时,需要行为一致、可重复的假服务:

  • 动作分步:先 open(打开/就绪)、再 getData(拉取证件照与文本信息)、必要时 close,与常见 SDK 调用顺序一致
  • 数据形态:返回体里带 NameIDCodeImageBase64String 等字段,前端或中间层可按真实协议解析
  • 技术选型:Express 负责路由与 JSON,几行 res.json 就能模拟成功/失败,配合 CORS 便于本地页面直接调

二、项目结构

路径说明
bin/www进程入口:创建 HTTP 服务并监听端口
app.js配置 CORS、解析大体积 JSON、挂载路由
routes/index.js浏览器访问根路径时的欢迎页(可选,用于确认服务已启动)
routes/users.js核心POST /server,按 header.handlerId 模拟读卡器各阶段响应
views/public/页面与静态资源
package.jsonnpm startnode ./bin/www

三、服务如何启动:bin/wwwapp.js

3.1 入口 bin/www:监听端口

package.json 指定 "start": "node ./bin/www"。这里完成三件事:

  1. require('../app') 加载 Express 应用;
  2. http.createServer(app) 创建 Node HTTP 服务;
  3. server.listen(port),本仓库默认 8075(可用环境变量 PORT 覆盖)。

bin/www 启动核心

// 引入在 app.js 中配置好的 Express 应用
var app = require('../app');
// debug 命名空间,开发时可通过 DEBUG 环境变量打开监听日志
var debug = require('debug')('express-app:server');
var http = require('http');

// 端口:优先读环境变量 PORT,否则用本仓库默认 8075;normalizePort 在同文件下方定义
var port = normalizePort(process.env.PORT || '8075');
app.set('port', port);

// 用 Node 原生 http 包装 Express,得到可 listen 的 Server 实例
var server = http.createServer(app);

server.listen(port);
server.on('error', onError);       // 端口占用、权限等错误
server.on('listening', onListening); // 成功监听后打印 debug 信息

3.2 app.js:中间件顺序(先跨域,再解析,再进读证件路由)

自助终端页面可能跑在不同端口或本地文件页,全局 CORS 让浏览器预检 OPTIONS 能通过;大体积 body 则对应带 Base64 头像等字段的 JSON。顺序仍是:先经过的中间件先执行

  1. 全局 CORSapp.all('*', ...) 写响应头;OPTIONS 直接 200,其它 next()
  2. 视图jade
  3. 日志与解析morganexpress.jsonbody-parser 等,保证 POST /server 能拿到 req.body
  4. 路由app.use('/server', usersRouter) —— 所有读证模拟接口都在此前缀下
  5. 404 / 错误页:未命中路由或异常时统一处理。

**app.js **:

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var bodyParser = require('body-parser');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

// ---------- 全局 CORS:终端页跨端口调用时常见 ----------
app.all('*', function(req, res, next) {
  res.header("Access-Control-Allow-Origin", '*');
  res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
  res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
  res.header("Access-Control-Allow-Credentials","true");
  res.header("X-Powered-By",' 3.2.1')
  if(req.method === "OPTIONS"){
    res.sendStatus(200); // 预检请求快速返回 200,不进入后续业务路由
  } else {
    next();
  }
});

// ---------- 模板:根路径欢迎页 ----------
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

// ---------- 通用中间件:日志、解析 JSON/表单、Cookie、静态资源 ----------
app.use(logger('dev'));
app.use(express.json({limit: '50mb'}));
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(bodyParser.json({
  limit: '10000kb'
}));
app.use(bodyParser.urlencoded({
  limit: '10000kb',
  extended: true,
  parameterLimit:5000000000,
}));

// ---------- 业务路由:/ 为欢迎页;/server 为读证模拟 ----------
app.use('/', indexRouter);
app.use('/server', usersRouter);

// ---------- 无匹配路由 → 404 ----------
app.use(function(req, res, next) {
  next(createError(404));
});

// ---------- 错误页 ----------
app.use(function(err, req, res, next) {
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

四、路由层

4.1 routes/index.js:确认服务已启动

GET / 渲染简单欢迎页,便于浏览器打开根地址判断 Express 是否起来。

var express = require('express');
var router = express.Router();

router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

module.exports = router;

4.2 routes/users.js:模拟自助终端读身份证(POST /server

URL 怎么拼
app.js 中为 app.use('/server', usersRouter)。子路由文件里写 router.post('/') 表示挂载在 /server 前缀之下,因此终端或前端应请求:

POST /server(示例:http://localhost:8075/server)。

请求体约定(与终端侧协议对齐)
发送 JSON,通过 req.body.header.handlerId 区分步骤,例如:

handlerId(示例)含义(模拟)
open打开读卡器 / 就绪
getData读取身份证芯片信息(姓名、身份证号、照片 Base64 等)
close关闭设备

核心代码

  • 一个 POST 入口模拟外设服务:getData 成功时返回 Body.result 中的证件字段;
  • 模块级变量 time(初值 20000)在每次成功读证后递减,用于模拟「前若干次与之后响应不同」等测试需求。
var express = require('express');
var router = express.Router();
// 模块级计数:配合 getData 模拟多次读证后的行为差异(完整见文件后半段)
var time = 20000;

// app.use('/server', usersRouter) → 此处 post('/') 即 POST /server
router.post('/', function(req, res, next) {
  console.log('time', time);
  console.log('req.header.handlerId', req.body.header.handlerId);

  if (req.body.header.handlerId === 'open') {
    // 模拟读卡器就绪
    res.json({
      header: {
        serviceId: "IDCardReaderServiceDemo1",
        time: "266ms",
        code: "100",
        msg: "",
        handlerId: "open"
      },
      Body: {
        code: "200",
        errorMsg: null,
        result: null
      }
    });
  } else if (req.body.header.handlerId === 'getData') {
    if (time > 0) {
      // 模拟二代证读证成功:固定演示数据
      res.json({
        header: {
          serviceId: "IDCardReaderServiceDemo2",
          time: "266ms",
          code: "000",
          msg: "",
          handlerId: "getData"
        },
        Body: {
          resCode: "200",
          errorMsg: null,
          result: {
            Name: "黑小虎",
            Sex: 1,
            Nation: 1,
            Birthday: "1900-10-19",
            Address: "XXXXXXXXXXXX",
            IDCode: "432301195705207528",
            Department: "***黑虎山",
            StartDate: "2008.03.04",
            EndDate: "2018.03.04",
            ImageBase64String: "图片base64String",
            FingerInfo: null,
            NewAddress: null,
            SecurityModuleNumber: null,
          }
        }
      });
      time--;
    } else {
      // ... time 耗尽时的响应,以及 close、打印等分支

五、小结

本次只是简单实现,后续还有很多点可以扩展,仅供大家参考。