const fs = require('fs') const path = require('path') const JSZip = require('jszip') const outputPath = path.resolve( __dirname, '..', 'doc', '大连元利流体技术有限公司MES测试环境接管报告_V1.1_2026-07-25.docx' ) const COLORS = { navy: '17365D', blue: '1F4E78', lightBlue: 'D9EAF7', paleBlue: 'EAF2F8', green: '548235', paleGreen: 'E2F0D9', amber: 'BF8F00', paleAmber: 'FFF2CC', red: 'C00000', paleRed: 'FCE4D6', gray: '666666', lightGray: 'F2F2F2', border: 'B4C6E7', white: 'FFFFFF' } function xmlEscape(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } function run(text, options = {}) { const props = [] const font = options.font || '宋体' const eastAsia = options.eastAsia || font props.push(``) if (options.bold) props.push('') if (options.italic) props.push('') if (options.color) props.push(``) if (options.size) props.push(``) if (options.highlight) props.push(``) return `${props.join('')}${xmlEscape(text)}` } function paragraph(content = '', options = {}) { const runs = Array.isArray(content) ? content : [run(content, options.run || {})] const props = [] if (options.style) props.push(``) if (options.align) props.push(``) if (options.keepNext) props.push('') if (options.keepLines) props.push('') if (options.pageBreakBefore) props.push('') if (options.spacingBefore != null || options.spacingAfter != null || options.line != null) { props.push(``) } if (options.indentLeft != null || options.firstLine != null) { props.push(``) } if (options.borderBottom) { props.push(``) } return `${props.join('')}${runs.join('')}` } function heading(text, level, options = {}) { return paragraph(text, { style: `Heading${level}`, keepNext: true, pageBreakBefore: Boolean(options.pageBreakBefore) }) } function bullet(text, level = 0) { return paragraph([ run('• ', { font: '微软雅黑', eastAsia: '微软雅黑', color: COLORS.blue, bold: true }), run(text, { font: '宋体', eastAsia: '宋体', size: 21 }) ], { indentLeft: 360 + level * 360, spacingAfter: 80, line: 320 }) } function numbered(number, text) { return paragraph([ run(`${number}. `, { font: '微软雅黑', eastAsia: '微软雅黑', color: COLORS.blue, bold: true, size: 21 }), run(text, { font: '宋体', eastAsia: '宋体', size: 21 }) ], { indentLeft: 360, spacingAfter: 100, line: 320 }) } function pageBreak() { return '' } function cell(content, width, options = {}) { const paragraphs = Array.isArray(content) ? content : [paragraph(String(content), { run: { font: options.font || '宋体', eastAsia: options.font || '宋体', size: options.size || 20, bold: Boolean(options.bold), color: options.color }, align: options.align, spacingAfter: 0, line: 280 })] const props = [ ``, '', `` ] if (options.fill) props.push(``) if (options.colSpan) props.push(``) return `${props.join('')}${paragraphs.join('')}` } function table(rows, widths, options = {}) { const grid = widths.map(width => ``).join('') const rowXml = rows.map((row, rowIndex) => { const isHeader = options.header && rowIndex === 0 const cells = row.map((value, index) => { const descriptor = value && typeof value === 'object' && !Array.isArray(value) ? value : { text: value } return cell(descriptor.paragraphs || descriptor.text, descriptor.width || widths[index], { fill: descriptor.fill || (isHeader ? COLORS.blue : (options.band && rowIndex % 2 === 0 ? COLORS.paleBlue : null)), bold: descriptor.bold != null ? descriptor.bold : isHeader, color: descriptor.color || (isHeader ? COLORS.white : null), align: descriptor.align || (isHeader ? 'center' : null), vAlign: descriptor.vAlign, colSpan: descriptor.colSpan, size: descriptor.size, font: descriptor.font }) }).join('') return `${isHeader ? '' : ''}${cells}` }).join('') return `${grid}${rowXml}` } function infoBox(title, text, type = 'info') { const palette = { info: [COLORS.blue, COLORS.paleBlue], success: [COLORS.green, COLORS.paleGreen], warning: [COLORS.amber, COLORS.paleAmber], danger: [COLORS.red, COLORS.paleRed] }[type] const paragraphs = [ paragraph([run(title, { font: '微软雅黑', eastAsia: '微软雅黑', bold: true, color: palette[0], size: 22 })], { spacingAfter: 80 }), paragraph([run(text, { font: '宋体', eastAsia: '宋体', size: 21 })], { spacingAfter: 0, line: 320 }) ] return table([[{ paragraphs, fill: palette[1] }]], [9000]) } function tocField() { return `目录将在打开文档时自动更新` } const body = [] // Cover body.push(paragraph('大连元利流体技术有限公司', { align: 'center', spacingBefore: 900, spacingAfter: 100, run: { font: '微软雅黑', eastAsia: '微软雅黑', size: 28, color: COLORS.gray } })) body.push(paragraph('MES 测试环境接管报告', { align: 'center', spacingBefore: 850, spacingAfter: 160, run: { font: '微软雅黑', eastAsia: '微软雅黑', size: 56, bold: true, color: COLORS.navy } })) body.push(paragraph('生产环境接管准备基线', { align: 'center', spacingAfter: 900, run: { font: '微软雅黑', eastAsia: '微软雅黑', size: 28, color: COLORS.blue } })) body.push(infoBox('接管结论', '测试环境已完成前端、MES 接口、数据库、只读外部集成、备份恢复与安全基线的受控接管,可作为后续生产环境接管的演练环境。', 'success')) body.push(paragraph('', { spacingAfter: 400 })) body.push(table([ ['文档编号', 'YL-MES-TAKEOVER-TEST-20260725'], ['文档版本', 'V1.1'], ['环境', 'MES 测试环境'], ['报告日期', '2026-07-25'], ['文档状态', '正式接管报告'], ['保密级别', '内部资料'] ], [2200, 6800], { band: true })) body.push(paragraph('本报告不包含账号、密码、Token 或生产业务明细。', { align: 'center', spacingBefore: 500, run: { font: '宋体', eastAsia: '宋体', size: 18, color: COLORS.gray } })) body.push(pageBreak()) // Document control and TOC body.push(heading('文档控制', 1)) body.push(table([ ['版本', '日期', '编制/维护', '变更说明'], ['V1.0', '2026-07-25', 'MES 接管维护', '首次形成测试环境接管报告'], ['V1.1', '2026-07-25', 'MES 接管维护', '修复部分富文本 XML 被显示为普通文字的问题'] ], [1200, 1600, 2000, 4200], { header: true, band: true })) body.push(heading('审批与分发', 2)) body.push(table([ ['角色', '姓名/部门', '确认内容', '状态'], ['业务负责人', '', '业务模块范围及验收口径', '待签字'], ['MES 运维负责人', '', '环境、发布、备份和回滚', '待签字'], ['信息安全负责人', '', '账号、网络、证书和审计要求', '待签字'], ['生产环境负责人', '', '生产接管窗口和授权边界', '待签字'] ], [1800, 2200, 3400, 1600], { header: true, band: true })) body.push(heading('目录', 1)) body.push(tocField()) body.push(pageBreak()) // 1 body.push(heading('1. 文档目的与接管结论', 1)) body.push(paragraph('本报告用于固化 MES 测试环境的实际接管状态、验证证据、运维能力和风险边界,并作为后续生产环境接管方案、演练和审批的输入。', { firstLine: 420, spacingAfter: 180, line: 360 })) body.push(infoBox('总体判定:受控接管完成', '测试前端已真实部署,MES 接口和数据库可管理,数据库恢复演练已通过,关键安全问题已整改,九个核心模块只读冒烟全部通过。当前环境可以承担日常诊断、受控变更、发布回归和生产接管演练。', 'success')) body.push(heading('1.1 接管状态摘要', 2)) body.push(table([ ['领域', '状态', '结论'], ['前端代码与部署', '已接管', '构建成功,IIS 10012 已部署并可访问'], ['MES 通用接口', '已接管', '登录、菜单、权限和只读过程调用通过'], ['测试数据库', '已接管', '结构诊断、脚本变更、回滚和恢复演练通过'], ['B1 集成', '只读接管', 'VPN 可达,语义 GET 实测通过,POST/PATCH 强制阻止'], ['TM 集成', '受限只读', '代理端口可达;当前无安全 GET 验收端点,库存任务写调用全部阻止'], ['核心后端', '发布物接管', '维护 IIS、配置和现有发布物;当前不修改核心 DLL'], ['安全基线', '部分完成', '密码响应、Cookie 和目录浏览已整改;证书与服务账号仍待处理'] ], [1900, 1700, 5400], { header: true, band: true })) // 2 body.push(heading('2. 接管范围与边界', 1, { pageBreakBefore: true })) body.push(heading('2.1 已纳入接管范围', 2)) ;[ 'Vue 2 管理前端代码、构建流程、运行配置和 IIS 前端站点。', 'MESCommonBase.ashx 通用接口的运行状态、配置、日志和已部署处理器。', '测试 SQL Server 2019 中 YL_MESDB 的结构、存储过程、索引、备份和恢复。', 'MES 登录、角色菜单、系统、工艺、计划、生产、质量、设备、异常、销售和看板查询链路。', 'B1/TM 代理的网络可用性和测试环境只读保护。', 'WinRM、IIS、部署目录、证书、目录浏览和防火墙等运维基线。' ].forEach(item => body.push(bullet(item))) body.push(heading('2.2 明确边界', 2)) ;[ '不在测试或生产环境执行未经批准的 MES 业务写入。', 'B1/TM 的语义 POST 和 PATCH 在测试前端被强制阻止,不进行真实库存、生产订单或条码任务写入。', '核心 DLL 没有原始源码,当前按发布物运维;本次不替换、不重建核心 DLL。', '本轮全模块验收为只读冒烟,不代表报工、送检、入库、领料和退料等写流程已经验收。', '正式环境的任何变更仍需单独授权、变更窗口、备份和回滚方案。' ].forEach(item => body.push(bullet(item))) // 3 body.push(heading('3. 环境与资产清单', 1, { pageBreakBefore: true })) body.push(heading('3.1 访问与部署地址', 2)) body.push(table([ ['对象', '地址/位置', '用途与状态'], ['测试前端', 'http://124.220.15.242:10012/', 'IIS 站点已启动,外部访问 200'], ['MES 测试接口', 'https://124.220.15.242:10101/submit/MESCommonBase.ashx', '业务接口可用;证书尚待整改'], ['测试数据库', '124.220.15.242,64722 / YL_MESDB', 'SQL Server 2019,数据库 ONLINE'], ['测试前端目录', 'C:\\ProjectShow\\元利流体\\MES_Manage_View\\dist', '84 个文件,约 95.69 MB'], ['后端发布目录', 'C:\\ProjectShow\\元利流体\\MES_ManageV20250916', '现有 10100/10101 IIS 发布物'], ['B1 代理', 'https://192.168.2.92:9996/b1s', '通过管理机 VPN 访问,仅允许语义 GET'], ['TM 代理', 'https://192.168.2.92:9996/openapi', '通过管理机 VPN 访问,写请求禁用'] ], [1900, 4300, 2800], { header: true, band: true })) body.push(heading('3.2 技术与版本基线', 2)) body.push(table([ ['层级', '版本/实现'], ['前端框架', 'Vue 2.7.16 / Vue Router 3.6.5 / Vuex 3.6.2'], ['UI 与 HTTP', 'Element UI 2.15.14 / Axios 0.21.4'], ['构建工具', 'Webpack 5.103.0 / Node.js 20'], ['数据库', 'Microsoft SQL Server 2019,兼容级别 150'], ['业务接口', 'ASP.NET .ashx 统一存储过程网关'], ['前端构建入口', 'app.83b8251a.js'], ['接管基线', 'MES测试环境接管基线_20260725.md / V1.3'] ], [2400, 6600], { band: true })) // 4 body.push(heading('4. 系统架构与调用链', 1, { pageBreakBefore: true })) body.push(table([[ { paragraphs: [ paragraph([run('浏览器 / Vue 前端', { font: 'Consolas', eastAsia: '微软雅黑', bold: true, color: COLORS.navy, size: 22 })], { align: 'center', spacingAfter: 100 }), paragraph([run('↓ 运行配置与 Axios', { font: 'Consolas', eastAsia: '微软雅黑', color: COLORS.gray, size: 20 })], { align: 'center', spacingAfter: 100 }), paragraph([run('MESCommonBase.ashx', { font: 'Consolas', eastAsia: '微软雅黑', bold: true, color: COLORS.blue, size: 22 })], { align: 'center', spacingAfter: 100 }), paragraph([run('↓ 存储过程名 + 参数 + 用户/模块上下文', { font: 'Consolas', eastAsia: '微软雅黑', color: COLORS.gray, size: 20 })], { align: 'center', spacingAfter: 100 }), paragraph([run('SQL Server / YL_MESDB', { font: 'Consolas', eastAsia: '微软雅黑', bold: true, color: COLORS.green, size: 22 })], { align: 'center', spacingAfter: 100 }), paragraph([run('↘ B1 代理(仅 GET) ↘ TM 代理(写入禁用)', { font: 'Consolas', eastAsia: '微软雅黑', color: COLORS.amber, size: 20 })], { align: 'center' }) ], fill: COLORS.lightGray } ]], [9000])) body.push(heading('4.1 环境隔离原则', 2)) body.push(paragraph('测试 dist 使用独立运行配置:MES 指向测试 10101;B1/TM 通过 VPN 指向现场代理;externalReadOnly 必须为 true。生产源配置保留 externalReadOnly=false,不将测试限制误带入正式构建。', { firstLine: 420, spacingAfter: 120, line: 360 })) body.push(infoBox('重要说明', 'B1/TM 代理封装在传输层统一使用 HTTP POST,但真正操作类型由请求体 func 决定。只读保护按语义 GET/POST/PATCH 判断,语义 GET 仍可通过代理传输。', 'warning')) // 5 body.push(heading('5. 已完成接管工作', 1, { pageBreakBefore: true })) body.push(heading('5.1 前端部署', 2)) ;[ '完成包含装配任务预算进度、登录安全修复和外部系统只读保护的生产构建。', '直接复制 dist 到测试服务器 staging,核对文件数、总字节数及 index.html/config.js SHA-256 后切换为最终目录。', '创建独立 IIS 站点和应用池,绑定 *:10012:,站点 Started、端口 Listening。', '服务器本机和管理端均返回 HTTP 200,实际加载 app.83b8251a.js。', '本次未生成 zip,符合直接部署 dist 的维护约定。' ].forEach(item => body.push(bullet(item))) body.push(heading('5.2 装配任务功能同步', 2)) ;[ '同步系统类和阀类装配任务合同工时修复、共享核心查询和 5 个查询索引。', '增加任务预算装配工时、合同预算测试工时及两个计算进度字段。', '测试环境跳过不可达的 SAP 交货日期远程读取,保留返回字段结构和独立回滚过程。', '系统类接口约 0.8-1.1 秒,阀类接口约 0.9-1.2 秒,均返回正确分页和预算字段。' ].forEach(item => body.push(bullet(item))) body.push(heading('5.3 安全整改', 2)) body.push(table([ ['问题', '整改结果', '验证证据'], ['登录接口返回 password', '登录过程显式返回非密码字段,保留密码校验', '登录成功,响应无 password 属性'], ['浏览器保存明文密码 Cookie', '只记住账号并删除旧 password Cookie', '源码和构建产物复核通过'], ['后端目录浏览开启', '根目录和 submit 均设置为 False', '目录返回 403,接口仍返回 200'], ['仓库历史口令残留', '历史注释和日志文字已脱敏', '已知口令特征扫描文件数为 0'], ['B1/TM 写污染风险', '测试配置启用语义写保护', '4 个写入口网络请求数为 0'] ], [2500, 3700, 2800], { header: true, band: true })) // 6 body.push(heading('6. 验证与演练结果', 1, { pageBreakBefore: true })) body.push(heading('6.1 核心模块只读冒烟', 2)) body.push(table([ ['模块', '查询入口', '结果', '耗时'], ['系统', '人员管理_人员信息查询', '200 / 135 行', '385 ms'], ['工艺', 'MES_ProcessManagement_ProcessGet', '200 / 合法 JSON', '113 ms'], ['计划', '计划排产_生产订单_工艺查询', '200 / 9878 行', '2619 ms'], ['生产', '生产管理_班次管理_班次列表查询', '200 / 4 行', '128 ms'], ['质量', '质量管理_测试装配任务_查询2', '200 / 0 行', '200 ms'], ['设备', '设备管理_设备数据_工位信息_查询', '200 / 21 行', '140 ms'], ['异常', '加急订单_加急明细查询', '200 / 合法 JSON', '117 ms'], ['销售', '销售管理_销售订单状态', '200 / 200 行', '430 ms'], ['首页看板', '看板_主页六个关键数据_查询', '200 / 25 行', '1099 ms'] ], [1300, 4200, 2100, 1400], { header: true, band: true })) body.push(paragraph('角色菜单查询返回 84 条,覆盖工艺、计划、生产、质量、设备和异常等主模块。质量测试装配任务当前返回 0 行属于有效空结果,不是接口失败。', { spacingBefore: 160, spacingAfter: 160, line: 340 })) body.push(heading('6.2 B1/TM 只读验证', 2)) body.push(table([ ['验证项', '结果'], ['VPN/代理端口', '192.168.2.92:9996 可达'], ['B1 ProductionOrders 最小 GET', 'HTTP 200,约 252 ms,包含 value,无 B1 错误'], ['B1 POST/PATCH', '前端拦截,网络请求 0'], ['TM POST/PATCH', '前端拦截,网络请求 0'], ['TM GET', '当前源码无明确安全 GET 端点,未调用库存任务接口'] ], [3000, 6000], { band: true })) body.push(heading('6.3 数据库备份恢复演练', 2)) body.push(table([ ['步骤', '结果', '关键数据'], ['备份头与文件列表读取', '通过', '完整备份约 1.28 GB,逻辑数据/日志文件完整'], ['RESTORE VERIFYONLY', '通过', '约 4.3 秒;历史备份未包含 checksum'], ['隔离恢复', '通过', 'YL_MESDB_RestoreDrill_20260725,约 9.4 秒 ONLINE'], ['DBCC CHECKDB PHYSICAL_ONLY', '通过', '约 5.9 秒,无物理一致性错误'], ['对象核对', '通过', '备份 61/27/425;当前 63/27/432'], ['演练清理', '通过', '演练库、MDF、LDF 均已删除,空间恢复'] ], [2800, 1600, 4600], { header: true, band: true })) body.push(infoBox('备份策略改进项', '当前完整备份没有启用 SQL Server checksum。普通 VERIFYONLY 和实际恢复均已通过,但生产备份任务应启用 WITH CHECKSUM,并定期执行独立恢复演练。', 'warning')) // 7 body.push(heading('7. 当前可开展的工作', 1, { pageBreakBefore: true })) ;[ '前端功能开发、生产构建、测试配置注入、IIS 发布和页面回归。', 'MES 登录、菜单、权限、接口错误和存储过程调用链诊断。', '测试数据库结构比对、查询性能分析、索引优化和带事务/回滚的变更。', '系统类、阀类装配任务查询、合同工时、预算工时和进度功能维护。', '通过 VPN 开展 B1 登录代理与 GET 查询联调,不污染现场业务数据。', '数据库备份校验、隔离恢复、DBCC 检查和清理演练。', 'IIS 站点、应用池、部署目录、日志、证书、WinRM 和防火墙巡检。', '为生产接管准备资产清单、变更脚本、回滚路径、验收记录和问题台账。' ].forEach((item, index) => body.push(numbered(index + 1, item))) body.push(infoBox('不能直接开展的工作', '未经专项授权不得执行生产环境变更;测试环境不执行 B1/TM 写入;MES 报工、送检、入库等写流程尚未完成系统性验收;核心 DLL 逻辑暂不修改。', 'danger')) // 8 body.push(heading('8. 日常运维与变更流程', 1, { pageBreakBefore: true })) body.push(heading('8.1 前端发布流程', 2)) ;[ '确认目标环境和变更范围,核对 Git 差异及运行配置。', '执行生产构建,检查构建错误和关键资源入口。', '构建后注入测试 config.js,确认 MES 地址和 externalReadOnly=true。', '直接复制 dist 到服务器 staging,不生成 zip。', '核对文件总数、总字节数和关键文件 SHA-256。', '切换 IIS 物理目录或最终 dist,验证首页、config.js、应用 Bundle 和登录。', '出现异常时恢复上一个 dist 目录并复测。' ].forEach((item, index) => body.push(numbered(index + 1, item))) body.push(heading('8.2 数据库变更流程', 2)) ;[ '部署前验证数据库、对象结构、依赖和当前定义哈希。', '每项变更必须包含事务、前置校验、验证 SQL 和回滚脚本。', '先在测试环境部署,执行接口、性能和数据清理回归。', '禁止使用测试库整体覆盖正式库,按对象脚本逐项同步。', '记录部署时间、执行人、对象、结果、回滚点和残留风险。' ].forEach((item, index) => body.push(numbered(index + 1, item))) body.push(heading('8.3 故障处理流程', 2)) body.push(table([ ['故障类型', '首要检查', '回退/处置'], ['前端无法访问', '10012 监听、站点状态、物理目录、首页文件', '启动站点或恢复上一版 dist'], ['接口失败', '10101、IIS 日志、证书、ASHX 响应、应用池', '回收应用池前先保留日志;必要时恢复发布物'], ['数据库慢查询', '执行计划、等待、索引、链接服务器', '终止异常诊断会话,回滚当次 SQL 变更'], ['B1/TM 异常', 'VPN、9996、func 类型、只读开关', '保持写保护,禁止以真实写入验证连通性'], ['数据恢复', '备份时间、VERIFYONLY、空间、逻辑文件名', '只恢复到隔离库,检查通过后再制定业务恢复方案'] ], [1800, 3700, 3500], { header: true, band: true })) // 9 body.push(heading('9. 生产环境接管准备', 1, { pageBreakBefore: true })) body.push(heading('9.1 建议接管阶段', 2)) body.push(table([ ['阶段', '主要工作', '完成标志'], ['阶段 1:只读盘点', '生产 IIS、数据库、配置、证书、备份、外部依赖和日志', '形成生产资产清单与差异清单'], ['阶段 2:权限与安全', '建立只读诊断账号、受控发布账号、管理入口白名单', '日常操作不再依赖 sysadmin/Administrator'], ['阶段 3:发布演练', '前端发布回滚、数据库脚本回滚、接口发布物恢复', '每类发布至少完成一次可重复演练'], ['阶段 4:业务验收', '只读模块、MES 写流程、B1/TM 受控联调', '关键业务流程有负责人和验收记录'], ['阶段 5:正式交接', '监控、巡检、值守、故障升级和文档签字', '接管责任、权限和窗口正式生效'] ], [1800, 4300, 2900], { header: true, band: true })) body.push(heading('9.2 生产变更前置条件', 2)) ;[ '用户明确批准生产变更范围和执行窗口。', '完成正式备份校验并确认可用回滚点。', '测试环境已通过同版本脚本和页面回归。', '配置文件不包含测试地址,B1/TM 保护策略符合生产业务要求。', '发布账号、数据库账号和远程管理账号遵循最小权限。', '发布后有业务负责人完成关键流程验收。' ].forEach(item => body.push(bullet(item))) // 10 body.push(heading('10. 风险与整改计划', 1, { pageBreakBefore: true })) body.push(table([ ['编号', '级别', '风险', '建议措施', '状态'], ['SEC-01', '高', '10101 使用名称不匹配的自签名证书', '部署受信任且匹配域名的业务证书', '待整改'], ['SEC-02', '高', '后端仍使用高权限数据库账号', '建立专用服务账号并完成全部写流程回归后切换', '待整改'], ['NET-01', '中', '10012 当前使用 HTTP 且来源为 Any', '启用 HTTPS,并按测试人员或网络范围限制入口', '待整改'], ['TEST-01', '高', 'MES 关键写流程尚未系统验收', '准备隔离测试数据、回滚脚本和业务验收人', '待验收'], ['INT-01', '中', 'TM 没有明确安全 GET 验收端点', '提供健康检查或只读查询端点', '待补充'], ['OPS-01', '中', '核心 DLL 没有原始源码', '日常按发布物运维;需要改逻辑时获取源码或专项验证替代构建', '已接受限制'], ['BAK-01', '中', '历史完整备份未启用 checksum', '调整备份任务并定期执行恢复演练', '待整改'], ['PERF-01', '低', '计划工艺查询返回量大且约 2.6 秒', '增加分页/条件限制并持续观察执行计划', '持续优化'] ], [1100, 900, 2500, 3400, 1100], { header: true, band: true })) // 11 body.push(heading('11. 回滚与应急依据', 1, { pageBreakBefore: true })) body.push(table([ ['对象', '回滚依据'], ['登录接口安全修复', 'rollback_secure_test_login_response_20260725.sql 及安全修复前备份过程'], ['装配任务查询优化', 'rollback_install_task_queries_20260725.sql'], ['装配任务查询索引', 'rollback_install_task_query_indexes_20260725.sql'], ['装配任务预算进度', 'rollback_install_task_budget_progress_20260725.sql'], ['测试 SAP 交货日期适配', 'rollback_test_disable_install_task_sap_delivery_20260725.sql'], ['后端目录浏览', '服务器同目录 web.config.directorybrowse_before_20260725.bak'], ['前端发布', '保留上一版 dist 或 staging,通过 IIS 物理路径切回'], ['数据库恢复', '完整备份 VERIFYONLY + 隔离恢复 + DBCC CHECKDB 流程'] ], [2700, 6300], { band: true })) body.push(infoBox('应急原则', '先保留证据和当前状态,再执行最小范围回退;不使用测试数据库整体覆盖正式数据库;不通过关闭只读保护来排查 B1/TM 连通性。', 'danger')) // 12 body.push(heading('12. 验收与签字', 1, { pageBreakBefore: true })) body.push(paragraph('各责任人签字表示已阅读本报告,认可当前测试环境的接管状态、能力范围、限制和生产接管前置条件。', { spacingAfter: 240, line: 340 })) body.push(table([ ['角色', '姓名', '意见', '签字', '日期'], ['业务负责人', '', '', '', ''], ['MES 运维负责人', '', '', '', ''], ['信息安全负责人', '', '', '', ''], ['生产环境负责人', '', '', '', ''] ], [1800, 1500, 2700, 1500, 1500], { header: true })) body.push(paragraph('', { spacingAfter: 300 })) body.push(infoBox('最终接管意见', '测试环境接管:通过(受控)。生产环境接管:尚未授权,需完成本报告第 9、10 节的前置条件与风险整改,并另行批准。', 'success')) // Appendix body.push(heading('附录 A:关键交付物', 1, { pageBreakBefore: true })) body.push(table([ ['类型', '交付物'], ['接管基线', 'doc/MES测试环境接管基线_20260725.md'], ['接管报告', 'doc/大连元利流体技术有限公司MES测试环境接管报告_V1.1_2026-07-25.docx'], ['过程日志', 'gptlog-process/gpdlog.md'], ['前端产物', 'dist/(含测试 static/config.js)'], ['登录安全脚本', 'db_backups/secure_test_login_response_20260725.sql 及回滚脚本'], ['装配任务脚本', 'db_backups/ 下 20260725 查询、索引、合同工时、预算进度及回滚脚本'], ['后端脱敏基线', 'server_baseline/MES_ManageV20250916_20260725/'] ], [2300, 6700], { band: true })) body.push(heading('附录 B:验证结论索引', 1)) ;[ '前端:10012 外部访问 200,配置 externalReadOnly=true。', '接口:登录 200,菜单 84 条,登录响应无 password。', '模块:9 个核心模块只读查询全部返回 200 和合法 JSON。', '外部集成:B1 GET 通过;B1/TM 写请求隔离测试网络调用为 0。', '数据库:VERIFYONLY、隔离恢复、DBCC CHECKDB、清理全部通过。', '安全:目录浏览关闭、密码 Cookie 移除、仓库已知口令特征扫描为 0。' ].forEach(item => body.push(bullet(item))) const sectionProperties = `` const documentXml = `${body.join('')}${sectionProperties}` const stylesXml = ` ` const settingsXml = `` const headerXml = `${run('大连元利流体技术有限公司 | MES 测试环境接管报告', { font: '微软雅黑', eastAsia: '微软雅黑', size: 17, color: COLORS.gray })}` const footerXml = `${run('内部资料 | 第 ', { font: '宋体', eastAsia: '宋体', size: 17, color: COLORS.gray })}1${run(' 页', { font: '宋体', eastAsia: '宋体', size: 17, color: COLORS.gray })}` const contentTypesXml = `` const packageRelsXml = `` const documentRelsXml = `` const coreXml = `大连元利流体技术有限公司MES测试环境接管报告MES 测试环境接管与生产接管准备MES 接管维护MES;测试环境;接管;IIS;数据库;恢复演练;安全MES 测试环境接管状态、验收证据、能力边界和生产接管准备报告。MES 接管维护2026-07-25T08:00:00Z2026-07-25T08:00:00Z1` const appXml = `Microsoft Office Word16.0000大连元利流体技术有限公司MES 运维0falsefalsefalsefalse` const zip = new JSZip() zip.file('[Content_Types].xml', contentTypesXml) zip.folder('_rels').file('.rels', packageRelsXml) zip.folder('docProps').file('core.xml', coreXml).file('app.xml', appXml) const word = zip.folder('word') word.file('document.xml', documentXml) word.file('styles.xml', stylesXml) word.file('settings.xml', settingsXml) word.file('header1.xml', headerXml) word.file('footer1.xml', footerXml) word.folder('_rels').file('document.xml.rels', documentRelsXml) fs.mkdirSync(path.dirname(outputPath), { recursive: true }) fs.writeFileSync(outputPath, zip.generate({ type: 'nodebuffer', compression: 'DEFLATE' })) const stats = fs.statSync(outputPath) console.log(JSON.stringify({ outputPath, bytes: stats.size, sections: 12, appendices: 2 }))