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 }))