灵活用工项目

generate-eid-doc.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. const fs = require('fs');
  2. const path = require('path');
  3. const { execSync } = require('child_process');
  4. const docDir = __dirname;
  5. const tmpDir = path.join(docDir, '_eid_docx_tmp');
  6. const docxPath = path.join(docDir, 'E证通小程序申请与配置指南.docx');
  7. function escapeXml(text) {
  8. return String(text)
  9. .replace(/&/g, '&')
  10. .replace(/</g, '&lt;')
  11. .replace(/>/g, '&gt;')
  12. .replace(/"/g, '&quot;');
  13. }
  14. function p(text, style, bold) {
  15. const styleXml = style ? `<w:pPr><w:pStyle w:val="${style}"/></w:pPr>` : '';
  16. const boldXml = bold ? '<w:b/>' : '';
  17. 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>`;
  18. }
  19. function h1(text) { return p(text, 'Heading1', true); }
  20. function h2(text) { return p(text, 'Heading2', true); }
  21. function body(text) { return p(text); }
  22. function bullet(text) {
  23. 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>`;
  24. }
  25. const sections = [
  26. h1('E证通小程序申请与配置指南'),
  27. body('适用项目:灵活用工微信小程序(uni-app)'),
  28. body('文档版本:v1.0'),
  29. body('编写日期:2026年7月8日'),
  30. body(''),
  31. h2('一、概述'),
  32. body('E证通(腾讯云慧眼人脸核身)在微信小程序中的接入方式,官方称为「E证通小程序 SDK」,并非微信公众平台「插件管理」中搜索添加的普通插件。'),
  33. body('整体接入分为两部分:'),
  34. bullet('腾讯云侧:申请商户 ID、下载 SDK、后端对接 GetEidToken / GetEidResult'),
  35. bullet('微信小程序后台:配置服务器域名、可选半屏小程序等'),
  36. body('官方文档:'),
  37. bullet('接入流程说明:https://cloud.tencent.com/document/product/1007/108009'),
  38. bullet('小程序集成:https://cloud.tencent.com/document/product/1007/108011'),
  39. body(''),
  40. h2('二、整体流程'),
  41. body('1. 腾讯云申请商户 ID(约 3 个工作日)'),
  42. body('2. 下载 E证通小程序 SDK(mp_ecard_sdk 文件夹)'),
  43. body('3. 后端接入 GetEidToken / GetEidResult 接口'),
  44. body('4. 微信小程序后台配置域名、半屏(可选)'),
  45. body('5. 小程序内集成 SDK 并调用 startEid 发起核身'),
  46. body(''),
  47. h2('三、腾讯云侧:申请商户 ID(必做)'),
  48. body('步骤 1:登录腾讯云慧眼 · 人脸核身控制台'),
  49. bullet('控制台地址:https://console.cloud.tencent.com/faceid'),
  50. body('步骤 2:进入「自助接入 → E证通服务 → 申请商户 ID」'),
  51. body('步骤 3:填写并提交申请资料'),
  52. bullet('小程序 AppID(manifest.json 中 mp-weixin.appid,不能为空)'),
  53. bullet('小程序名称'),
  54. bullet('业务场景说明(例如:临时工/企业注册实人认证)'),
  55. bullet('按提示上传业务域名校验文件(若需要返回身份信息等)'),
  56. body('步骤 4:等待审核,一般约 3 个工作日'),
  57. body('步骤 5:审核通过后'),
  58. bullet('获得商户 ID(MerchantId)'),
  59. bullet('可下载 E证通小程序 SDK(mp_ecard_sdk 文件夹)'),
  60. body('后端使用商户 ID 调用 GetEidToken 生成核身 Token,再传给前端使用。'),
  61. body(''),
  62. h2('四、微信小程序后台配置'),
  63. body('登录微信公众平台:https://mp.weixin.qq.com/ ,进入对应小程序。'),
  64. body(''),
  65. body('4.1 服务器域名(必配)'),
  66. body('路径:开发 → 开发管理 → 开发设置 → 服务器域名 → request 合法域名'),
  67. body('添加域名:https://eid.faceid.qq.com'),
  68. body('说明:开发阶段可在微信开发者工具勾选「不校验合法域名」,但上线前必须配置。'),
  69. body(''),
  70. body('4.2 业务域名(视 SDK 版本而定)'),
  71. bullet('SDK V1.0.7 及以上:一般不需要配置业务域名'),
  72. bullet('更低版本:可能需要将 eid.faceid.qq.com 加入业务域名,校验文件在申请商户 ID 时一并提交审核'),
  73. body(''),
  74. body('4.3 半屏打开 eID 小程序(可选,体验更好)'),
  75. body('E证通核身时会跳转到「eID 数字身份」小程序,AppID:wx0e2cb0b052a91c92'),
  76. body('配置路径:设置 → 第三方设置 → 半屏小程序管理 → 添加'),
  77. bullet('每个小程序最多可添加 10 个半屏小程序'),
  78. bullet('还需向 E证通侧申请半屏权限,审核通过后才会生效'),
  79. bullet('不支持半屏时会自动降级为全屏跳转'),
  80. body('半屏模式还需在 app.json 中配置(uni-app 对应 pages.json / manifest.json):'),
  81. body('"embeddedAppIdList": ["wx0e2cb0b052a91c92"]'),
  82. body(''),
  83. body('4.4 关于「插件」的说明'),
  84. body('E证通不需要在「设置 → 第三方设置 → 插件管理」中搜索添加插件。正确做法是:下载 SDK 放入项目,在 App.onLaunch 中调用 initEid,页面中通过按钮点击调用 startEid 并传入 token。'),
  85. body(''),
  86. h2('五、小程序 SDK 集成要点'),
  87. body('5.1 下载并放置 SDK'),
  88. body('在腾讯云控制台下载 E证通小程序 SDK,将 mp_ecard_sdk 文件夹放在小程序根目录下(uni-app 项目建议放在 app/ 目录下)。'),
  89. body(''),
  90. body('5.2 初始化 SDK(App.onLaunch)'),
  91. body('在 App.js / App.vue 的 onLaunch 中引入并调用 initEid。'),
  92. body('在 pages.json 中注册 SDK 自带页面路径(以 SDK 版本说明为准)。'),
  93. body(''),
  94. body('5.3 调用核身(必须通过按钮点击触发)'),
  95. body('SDK v1.0.5 及以上版本,startEid 必须通过按钮点击形式发起(内部使用 wx.navigateToMiniProgram 拉起 eID 数字身份小程序)。'),
  96. body('主要参数说明:'),
  97. bullet('data.token:后端返回的 EidToken(必填)'),
  98. bullet('data.enableEmbedded:是否半屏打开(SDK v1.0.9+,默认 false)'),
  99. bullet('data.allowFullScreen:半屏后是否允许全屏(SDK v1.0.9+,默认 true)'),
  100. bullet('verifyDoneCallback:核身完成回调,含 token 与 verifyDone 标志'),
  101. body(''),
  102. body('5.4 从 eID 小程序返回时的 onShow 处理'),
  103. body('用户从 eID 数字身份小程序返回时,会同时触发接入方 App.onShow。若 onShow 中有登录跳转等逻辑,需排除此场景:'),
  104. body('判断条件:scene === 1038 且 referrerInfo.appId === "wx0e2cb0b052a91c92" 时直接 return,不执行原有逻辑。'),
  105. body(''),
  106. h2('六、后端接口要求'),
  107. body('商户 ID 审核通过后,后端需至少提供以下能力:'),
  108. bullet('GetEidToken:根据姓名、身份证号等信息向腾讯云换取 EidToken,返回给前端'),
  109. bullet('GetEidResult:核身结束后查询核验结果,以前端回调为辅、后端查询为准'),
  110. body('默认情况下不会返回姓名、身份证等明文信息;如需加密返回身份信息,需在控制台单独申请开通。'),
  111. body('建议后端封装接口示例:'),
  112. bullet('POST /api/v1/mp/face/init — 登记提交后获取 EidToken'),
  113. bullet('GET /api/v1/mp/face/result — 核身完成后查询结果并更新 faceVerified 状态'),
  114. body(''),
  115. h2('七、与本项目的对应关系'),
  116. bullet('小程序 AppID:manifest.json → mp-weixin.appid(当前需填写)'),
  117. bullet('SDK 目录:app/mp_ecard_sdk/'),
  118. bullet('初始化:App.vue 的 onLaunch 中 initEid'),
  119. bullet('页面注册:pages.json 增加 SDK 页面路径'),
  120. bullet('业务串联:企业/临时工登记页提交 → 人脸校验页 → startEid → 后端确认 → 进入业务首页'),
  121. body('建议业务流程:'),
  122. body('登记页提交 → 后端保存资料并 GetEidToken → 跳转人脸校验页 → 用户点击按钮 startEid(token) → 进入 eID 数字身份小程序 → 返回本小程序 → 后端 GetEidResult 确认 → faceVerified = true → 进入首页'),
  123. body(''),
  124. h2('八、调试与注意事项'),
  125. bullet('请在微信开发者工具中使用手机「预览」模式调试,官方建议不要使用「真机调试」'),
  126. bullet('E证通 SDK 通过 wx.onAppShow 监听从 eID 小程序返回的事件,触发 verifyDoneCallback'),
  127. bullet('半屏接入是较新能力,部分机型存在兼容性问题,上线前需充分测试'),
  128. body(''),
  129. h2('九、申请前 Checklist'),
  130. bullet('小程序已注册,AppID 已填入 manifest.json'),
  131. bullet('腾讯云账号已实名认证'),
  132. bullet('人脸核身 / E证通 服务已开通'),
  133. bullet('商户 ID 已申请(约 3 工作日)'),
  134. bullet('E证通小程序 SDK 已下载'),
  135. bullet('微信后台 request 域名已添加 eid.faceid.qq.com'),
  136. bullet('(可选)半屏小程序已配置 wx0e2cb0b052a91c92'),
  137. bullet('后端 GetEidToken / GetEidResult 接口已联调通过'),
  138. body(''),
  139. h2('十、常见问题'),
  140. body('Q1:找不到「E证通插件」怎么办?'),
  141. body('A:E证通不是插件市场插件,需在腾讯云下载 SDK 集成,并在微信后台配置域名。'),
  142. body(''),
  143. body('Q2:调起核身失败?'),
  144. body('A:检查 AppID 是否为空、商户 ID 是否审核通过、是否已 initEid、Token 是否有效。'),
  145. body(''),
  146. body('Q3:域名请求报错?'),
  147. body('A:确认已在微信后台添加 https://eid.faceid.qq.com 到 request 合法域名。'),
  148. body(''),
  149. body('Q4:真机调试异常?'),
  150. body('A:改用开发者工具「预览」扫码测试,不要使用「真机调试」。'),
  151. body(''),
  152. body('Q5:返回小程序后页面乱跳?'),
  153. body('A:在 App.onShow 中排除从 eID 小程序返回的场景(scene=1038)。'),
  154. ];
  155. const documentXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  156. <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  157. <w:body>
  158. ${sections.join('\n ')}
  159. <w:sectPr>
  160. <w:pgSz w:w="11906" w:h="16838"/>
  161. <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/>
  162. </w:sectPr>
  163. </w:body>
  164. </w:document>`;
  165. const files = {
  166. '[Content_Types].xml': `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  167. <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  168. <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  169. <Default Extension="xml" ContentType="application/xml"/>
  170. <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
  171. <Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>
  172. <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
  173. </Types>`,
  174. '_rels/.rels': `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  175. <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  176. <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
  177. </Relationships>`,
  178. 'word/document.xml': documentXml,
  179. 'word/_rels/document.xml.rels': `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  180. <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  181. <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>
  182. <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
  183. </Relationships>`,
  184. 'word/numbering.xml': `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  185. <w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  186. <w:abstractNum w:abstractNumId="0">
  187. <w:multiLevelType w:val="hybridMultilevel"/>
  188. <w:lvl w:ilvl="0">
  189. <w:start w:val="1"/>
  190. <w:numFmt w:val="bullet"/>
  191. <w:lvlText w:val="•"/>
  192. <w:lvlJc w:val="left"/>
  193. <w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr>
  194. </w:lvl>
  195. </w:abstractNum>
  196. <w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>
  197. </w:numbering>`,
  198. 'word/styles.xml': `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  199. <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  200. <w:style w:type="paragraph" w:default="1" w:styleId="Normal">
  201. <w:name w:val="Normal"/>
  202. <w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="24"/></w:rPr>
  203. </w:style>
  204. <w:style w:type="paragraph" w:styleId="Heading1">
  205. <w:name w:val="heading 1"/>
  206. <w:pPr><w:spacing w:before="240" w:after="120"/></w:pPr>
  207. <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>
  208. </w:style>
  209. <w:style w:type="paragraph" w:styleId="Heading2">
  210. <w:name w:val="heading 2"/>
  211. <w:pPr><w:spacing w:before="200" w:after="100"/></w:pPr>
  212. <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>
  213. </w:style>
  214. <w:style w:type="paragraph" w:styleId="ListParagraph">
  215. <w:name w:val="List Paragraph"/>
  216. <w:pPr><w:ind w:left="720"/></w:pPr>
  217. </w:style>
  218. </w:styles>`,
  219. };
  220. if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
  221. fs.mkdirSync(tmpDir, { recursive: true });
  222. for (const [relPath, content] of Object.entries(files)) {
  223. const fullPath = path.join(tmpDir, relPath);
  224. fs.mkdirSync(path.dirname(fullPath), { recursive: true });
  225. fs.writeFileSync(fullPath, content, 'utf8');
  226. }
  227. if (fs.existsSync(docxPath)) fs.unlinkSync(docxPath);
  228. const ps1Path = path.join(docDir, '_zip-eid-doc.ps1');
  229. const ps1 = `
  230. $ErrorActionPreference = 'Stop'
  231. Add-Type -AssemblyName System.IO.Compression.FileSystem
  232. $tmp = '${tmpDir.replace(/'/g, "''")}'
  233. $out = '${docxPath.replace(/'/g, "''")}'
  234. if (Test-Path -LiteralPath $out) { Remove-Item -LiteralPath $out -Force }
  235. [System.IO.Compression.ZipFile]::CreateFromDirectory($tmp, $out)
  236. Write-Host "OK"
  237. `;
  238. fs.writeFileSync(ps1Path, '\ufeff' + ps1, 'utf8');
  239. execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}"`, { stdio: 'inherit' });
  240. fs.unlinkSync(ps1Path);
  241. fs.rmSync(tmpDir, { recursive: true, force: true });
  242. if (!fs.existsSync(docxPath)) {
  243. console.error('Failed to create docx');
  244. process.exit(1);
  245. }
  246. console.log('Generated:', docxPath);
  247. console.log('Size:', fs.statSync(docxPath).size, 'bytes');