const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const docDir = __dirname;
const tmpDir = path.join(docDir, '_eid_docx_tmp');
const docxPath = path.join(docDir, 'E证通小程序申请与配置指南.docx');
function escapeXml(text) {
return String(text)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function p(text, style, bold) {
const styleXml = style ? `` : '';
const boldXml = bold ? '' : '';
return `${styleXml}${boldXml}${escapeXml(text)}`;
}
function h1(text) { return p(text, 'Heading1', true); }
function h2(text) { return p(text, 'Heading2', true); }
function body(text) { return p(text); }
function bullet(text) {
return `${escapeXml(text)}`;
}
const sections = [
h1('E证通小程序申请与配置指南'),
body('适用项目:灵活用工微信小程序(uni-app)'),
body('文档版本:v1.0'),
body('编写日期:2026年7月8日'),
body(''),
h2('一、概述'),
body('E证通(腾讯云慧眼人脸核身)在微信小程序中的接入方式,官方称为「E证通小程序 SDK」,并非微信公众平台「插件管理」中搜索添加的普通插件。'),
body('整体接入分为两部分:'),
bullet('腾讯云侧:申请商户 ID、下载 SDK、后端对接 GetEidToken / GetEidResult'),
bullet('微信小程序后台:配置服务器域名、可选半屏小程序等'),
body('官方文档:'),
bullet('接入流程说明:https://cloud.tencent.com/document/product/1007/108009'),
bullet('小程序集成:https://cloud.tencent.com/document/product/1007/108011'),
body(''),
h2('二、整体流程'),
body('1. 腾讯云申请商户 ID(约 3 个工作日)'),
body('2. 下载 E证通小程序 SDK(mp_ecard_sdk 文件夹)'),
body('3. 后端接入 GetEidToken / GetEidResult 接口'),
body('4. 微信小程序后台配置域名、半屏(可选)'),
body('5. 小程序内集成 SDK 并调用 startEid 发起核身'),
body(''),
h2('三、腾讯云侧:申请商户 ID(必做)'),
body('步骤 1:登录腾讯云慧眼 · 人脸核身控制台'),
bullet('控制台地址:https://console.cloud.tencent.com/faceid'),
body('步骤 2:进入「自助接入 → E证通服务 → 申请商户 ID」'),
body('步骤 3:填写并提交申请资料'),
bullet('小程序 AppID(manifest.json 中 mp-weixin.appid,不能为空)'),
bullet('小程序名称'),
bullet('业务场景说明(例如:临时工/企业注册实人认证)'),
bullet('按提示上传业务域名校验文件(若需要返回身份信息等)'),
body('步骤 4:等待审核,一般约 3 个工作日'),
body('步骤 5:审核通过后'),
bullet('获得商户 ID(MerchantId)'),
bullet('可下载 E证通小程序 SDK(mp_ecard_sdk 文件夹)'),
body('后端使用商户 ID 调用 GetEidToken 生成核身 Token,再传给前端使用。'),
body(''),
h2('四、微信小程序后台配置'),
body('登录微信公众平台:https://mp.weixin.qq.com/ ,进入对应小程序。'),
body(''),
body('4.1 服务器域名(必配)'),
body('路径:开发 → 开发管理 → 开发设置 → 服务器域名 → request 合法域名'),
body('添加域名:https://eid.faceid.qq.com'),
body('说明:开发阶段可在微信开发者工具勾选「不校验合法域名」,但上线前必须配置。'),
body(''),
body('4.2 业务域名(视 SDK 版本而定)'),
bullet('SDK V1.0.7 及以上:一般不需要配置业务域名'),
bullet('更低版本:可能需要将 eid.faceid.qq.com 加入业务域名,校验文件在申请商户 ID 时一并提交审核'),
body(''),
body('4.3 半屏打开 eID 小程序(可选,体验更好)'),
body('E证通核身时会跳转到「eID 数字身份」小程序,AppID:wx0e2cb0b052a91c92'),
body('配置路径:设置 → 第三方设置 → 半屏小程序管理 → 添加'),
bullet('每个小程序最多可添加 10 个半屏小程序'),
bullet('还需向 E证通侧申请半屏权限,审核通过后才会生效'),
bullet('不支持半屏时会自动降级为全屏跳转'),
body('半屏模式还需在 app.json 中配置(uni-app 对应 pages.json / manifest.json):'),
body('"embeddedAppIdList": ["wx0e2cb0b052a91c92"]'),
body(''),
body('4.4 关于「插件」的说明'),
body('E证通不需要在「设置 → 第三方设置 → 插件管理」中搜索添加插件。正确做法是:下载 SDK 放入项目,在 App.onLaunch 中调用 initEid,页面中通过按钮点击调用 startEid 并传入 token。'),
body(''),
h2('五、小程序 SDK 集成要点'),
body('5.1 下载并放置 SDK'),
body('在腾讯云控制台下载 E证通小程序 SDK,将 mp_ecard_sdk 文件夹放在小程序根目录下(uni-app 项目建议放在 app/ 目录下)。'),
body(''),
body('5.2 初始化 SDK(App.onLaunch)'),
body('在 App.js / App.vue 的 onLaunch 中引入并调用 initEid。'),
body('在 pages.json 中注册 SDK 自带页面路径(以 SDK 版本说明为准)。'),
body(''),
body('5.3 调用核身(必须通过按钮点击触发)'),
body('SDK v1.0.5 及以上版本,startEid 必须通过按钮点击形式发起(内部使用 wx.navigateToMiniProgram 拉起 eID 数字身份小程序)。'),
body('主要参数说明:'),
bullet('data.token:后端返回的 EidToken(必填)'),
bullet('data.enableEmbedded:是否半屏打开(SDK v1.0.9+,默认 false)'),
bullet('data.allowFullScreen:半屏后是否允许全屏(SDK v1.0.9+,默认 true)'),
bullet('verifyDoneCallback:核身完成回调,含 token 与 verifyDone 标志'),
body(''),
body('5.4 从 eID 小程序返回时的 onShow 处理'),
body('用户从 eID 数字身份小程序返回时,会同时触发接入方 App.onShow。若 onShow 中有登录跳转等逻辑,需排除此场景:'),
body('判断条件:scene === 1038 且 referrerInfo.appId === "wx0e2cb0b052a91c92" 时直接 return,不执行原有逻辑。'),
body(''),
h2('六、后端接口要求'),
body('商户 ID 审核通过后,后端需至少提供以下能力:'),
bullet('GetEidToken:根据姓名、身份证号等信息向腾讯云换取 EidToken,返回给前端'),
bullet('GetEidResult:核身结束后查询核验结果,以前端回调为辅、后端查询为准'),
body('默认情况下不会返回姓名、身份证等明文信息;如需加密返回身份信息,需在控制台单独申请开通。'),
body('建议后端封装接口示例:'),
bullet('POST /api/v1/mp/face/init — 登记提交后获取 EidToken'),
bullet('GET /api/v1/mp/face/result — 核身完成后查询结果并更新 faceVerified 状态'),
body(''),
h2('七、与本项目的对应关系'),
bullet('小程序 AppID:manifest.json → mp-weixin.appid(当前需填写)'),
bullet('SDK 目录:app/mp_ecard_sdk/'),
bullet('初始化:App.vue 的 onLaunch 中 initEid'),
bullet('页面注册:pages.json 增加 SDK 页面路径'),
bullet('业务串联:企业/临时工登记页提交 → 人脸校验页 → startEid → 后端确认 → 进入业务首页'),
body('建议业务流程:'),
body('登记页提交 → 后端保存资料并 GetEidToken → 跳转人脸校验页 → 用户点击按钮 startEid(token) → 进入 eID 数字身份小程序 → 返回本小程序 → 后端 GetEidResult 确认 → faceVerified = true → 进入首页'),
body(''),
h2('八、调试与注意事项'),
bullet('请在微信开发者工具中使用手机「预览」模式调试,官方建议不要使用「真机调试」'),
bullet('E证通 SDK 通过 wx.onAppShow 监听从 eID 小程序返回的事件,触发 verifyDoneCallback'),
bullet('半屏接入是较新能力,部分机型存在兼容性问题,上线前需充分测试'),
body(''),
h2('九、申请前 Checklist'),
bullet('小程序已注册,AppID 已填入 manifest.json'),
bullet('腾讯云账号已实名认证'),
bullet('人脸核身 / E证通 服务已开通'),
bullet('商户 ID 已申请(约 3 工作日)'),
bullet('E证通小程序 SDK 已下载'),
bullet('微信后台 request 域名已添加 eid.faceid.qq.com'),
bullet('(可选)半屏小程序已配置 wx0e2cb0b052a91c92'),
bullet('后端 GetEidToken / GetEidResult 接口已联调通过'),
body(''),
h2('十、常见问题'),
body('Q1:找不到「E证通插件」怎么办?'),
body('A:E证通不是插件市场插件,需在腾讯云下载 SDK 集成,并在微信后台配置域名。'),
body(''),
body('Q2:调起核身失败?'),
body('A:检查 AppID 是否为空、商户 ID 是否审核通过、是否已 initEid、Token 是否有效。'),
body(''),
body('Q3:域名请求报错?'),
body('A:确认已在微信后台添加 https://eid.faceid.qq.com 到 request 合法域名。'),
body(''),
body('Q4:真机调试异常?'),
body('A:改用开发者工具「预览」扫码测试,不要使用「真机调试」。'),
body(''),
body('Q5:返回小程序后页面乱跳?'),
body('A:在 App.onShow 中排除从 eID 小程序返回的场景(scene=1038)。'),
];
const documentXml = `
${sections.join('\n ')}
`;
const files = {
'[Content_Types].xml': `
`,
'_rels/.rels': `
`,
'word/document.xml': documentXml,
'word/_rels/document.xml.rels': `
`,
'word/numbering.xml': `
`,
'word/styles.xml': `
`,
};
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
fs.mkdirSync(tmpDir, { recursive: true });
for (const [relPath, content] of Object.entries(files)) {
const fullPath = path.join(tmpDir, relPath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content, 'utf8');
}
if (fs.existsSync(docxPath)) fs.unlinkSync(docxPath);
const ps1Path = path.join(docDir, '_zip-eid-doc.ps1');
const ps1 = `
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.IO.Compression.FileSystem
$tmp = '${tmpDir.replace(/'/g, "''")}'
$out = '${docxPath.replace(/'/g, "''")}'
if (Test-Path -LiteralPath $out) { Remove-Item -LiteralPath $out -Force }
[System.IO.Compression.ZipFile]::CreateFromDirectory($tmp, $out)
Write-Host "OK"
`;
fs.writeFileSync(ps1Path, '\ufeff' + ps1, 'utf8');
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}"`, { stdio: 'inherit' });
fs.unlinkSync(ps1Path);
fs.rmSync(tmpDir, { recursive: true, force: true });
if (!fs.existsSync(docxPath)) {
console.error('Failed to create docx');
process.exit(1);
}
console.log('Generated:', docxPath);
console.log('Size:', fs.statSync(docxPath).size, 'bytes');