| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- 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, '>')
- .replace(/"/g, '"');
- }
- function p(text, style, bold) {
- const styleXml = style ? `<w:pPr><w:pStyle w:val="${style}"/></w:pPr>` : '';
- const boldXml = bold ? '<w:b/>' : '';
- return `<w:p>${styleXml}<w:r><w:rPr>${boldXml}<w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="24"/></w:rPr><w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r></w:p>`;
- }
- 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 `<w:p><w:pPr><w:pStyle w:val="ListParagraph"/><w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr><w:r><w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="24"/></w:rPr><w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r></w:p>`;
- }
- 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 = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
- <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
- <w:body>
- ${sections.join('\n ')}
- <w:sectPr>
- <w:pgSz w:w="11906" w:h="16838"/>
- <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/>
- </w:sectPr>
- </w:body>
- </w:document>`;
- const files = {
- '[Content_Types].xml': `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
- <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
- <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
- <Default Extension="xml" ContentType="application/xml"/>
- <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
- <Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>
- <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
- </Types>`,
- '_rels/.rels': `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
- <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
- <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
- </Relationships>`,
- 'word/document.xml': documentXml,
- 'word/_rels/document.xml.rels': `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
- <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
- <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>
- <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
- </Relationships>`,
- 'word/numbering.xml': `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
- <w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
- <w:abstractNum w:abstractNumId="0">
- <w:multiLevelType w:val="hybridMultilevel"/>
- <w:lvl w:ilvl="0">
- <w:start w:val="1"/>
- <w:numFmt w:val="bullet"/>
- <w:lvlText w:val="•"/>
- <w:lvlJc w:val="left"/>
- <w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr>
- </w:lvl>
- </w:abstractNum>
- <w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>
- </w:numbering>`,
- 'word/styles.xml': `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
- <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
- <w:style w:type="paragraph" w:default="1" w:styleId="Normal">
- <w:name w:val="Normal"/>
- <w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="24"/></w:rPr>
- </w:style>
- <w:style w:type="paragraph" w:styleId="Heading1">
- <w:name w:val="heading 1"/>
- <w:pPr><w:spacing w:before="240" w:after="120"/></w:pPr>
- <w:rPr><w:b/><w:sz w:val="32"/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/></w:rPr>
- </w:style>
- <w:style w:type="paragraph" w:styleId="Heading2">
- <w:name w:val="heading 2"/>
- <w:pPr><w:spacing w:before="200" w:after="100"/></w:pPr>
- <w:rPr><w:b/><w:sz w:val="28"/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/></w:rPr>
- </w:style>
- <w:style w:type="paragraph" w:styleId="ListParagraph">
- <w:name w:val="List Paragraph"/>
- <w:pPr><w:ind w:left="720"/></w:pPr>
- </w:style>
- </w:styles>`,
- };
- 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');
|